diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_http_service |
Publish
Diffstat (limited to 'jdisc_http_service')
166 files changed, 20604 insertions, 0 deletions
diff --git a/jdisc_http_service/.gitignore b/jdisc_http_service/.gitignore new file mode 100644 index 00000000000..3cc25b51fc4 --- /dev/null +++ b/jdisc_http_service/.gitignore @@ -0,0 +1,2 @@ +/pom.xml.build +/target diff --git a/jdisc_http_service/OWNERS b/jdisc_http_service/OWNERS new file mode 100644 index 00000000000..5255d2560bb --- /dev/null +++ b/jdisc_http_service/OWNERS @@ -0,0 +1,2 @@ +bakksjo +gjoranv diff --git a/jdisc_http_service/README.sh b/jdisc_http_service/README.sh new file mode 100755 index 00000000000..b17f71e119f --- /dev/null +++ b/jdisc_http_service/README.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +CURRENT=$(date -u '+%F %T %Z') + +if [ -z ${VERSION} ]; then + echo "ERROR: No version number defined"; + exit 1; +fi + +cat <<EOF + +This package provides a ClientProvider and a ServerProvider implementation using HTTP on JDisc. + +EOF diff --git a/jdisc_http_service/docs/class-diagram.graffle b/jdisc_http_service/docs/class-diagram.graffle new file mode 100644 index 00000000000..938459c6571 --- /dev/null +++ b/jdisc_http_service/docs/class-diagram.graffle @@ -0,0 +1,1856 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>ActiveLayerIndex</key> + <integer>0</integer> + <key>ApplicationVersion</key> + <array> + <string>com.omnigroup.OmniGrafflePro</string> + <string>139.7.0.167456</string> + </array> + <key>AutoAdjust</key> + <true/> + <key>BackgroundGraphic</key> + <dict> + <key>Bounds</key> + <string>{{0, 0}, {558.99999713897705, 783}}</string> + <key>Class</key> + <string>SolidGraphic</string> + <key>ID</key> + <integer>2</integer> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + </dict> + <key>BaseZoom</key> + <integer>0</integer> + <key>CanvasOrigin</key> + <string>{0, 0}</string> + <key>ColumnAlign</key> + <integer>1</integer> + <key>ColumnSpacing</key> + <real>36</real> + <key>CreationDate</key> + <string>2012-06-18 12:41:37 +0000</string> + <key>Creator</key> + <string>Einar Rosenvinge</string> + <key>DisplayScale</key> + <string>1.000 cm = 1.000 cm</string> + <key>GraphDocumentVersion</key> + <integer>8</integer> + <key>GraphicsList</key> + <array> + <dict> + <key>Bounds</key> + <string>{{404.66666889190674, 39.999999999999936}, {151.17318725585938, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>114</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 J2SE API}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{404.66666889190674, 18.66666666666665}, {151.17318725585938, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>109</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Netty API}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>108</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{404.66666666666731, 7.9472862957175039e-08}, {151.17318725585938, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>107</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>1</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 jDISC core API}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{328.46341597965198, 296.25884156306495}, {29.333332061767578, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>106</integer> + <key>Line</key> + <dict> + <key>ID</key> + <integer>105</integer> + <key>Offset</key> + <real>6.6666665077209473</real> + <key>Position</key> + <real>0.91559326648712158</real> + <key>RotationType</key> + <integer>0</integer> + </dict> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 1}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>52</integer> + </dict> + <key>ID</key> + <integer>105</integer> + <key>Points</key> + <array> + <string>{225.55682373046864, 284.810302734375}</string> + <string>{352.43905966196957, 312.07829430296113}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>StickArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>104</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{101.99999999999997, 277.810302734375}, {123.55682373046864, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>104</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 ChannelPipeline}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.25436339285309, 474.88842165638926}, {29.333332061767578, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>103</integer> + <key>Line</key> + <dict> + <key>ID</key> + <integer>102</integer> + <key>Offset</key> + <real>6.6666665077209473</real> + <key>Position</key> + <real>0.78203368186950684</real> + <key>RotationType</key> + <integer>0</integer> + </dict> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 1}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>89</integer> + </dict> + <key>ID</key> + <integer>102</integer> + <key>Points</key> + <array> + <string>{291.52424638132226, 513.75253787937902}</string> + <string>{148.21246360738095, 481.32189037141075}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>StickArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>58</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>95</integer> + </dict> + <key>ID</key> + <integer>96</integer> + <key>Points</key> + <array> + <string>{116.77841269969511, 466.71157835576878}</string> + <string>{116.77840998702608, 420.16666668003933}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>89</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{58.945077896118171, 405.66666666666708}, {115.66666412353516, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>95</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>1</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Request}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{68.3333333333334, 467.21157836914102}, {96.890159606933594, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>89</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 HttpRequest}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>88</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>86</integer> + </dict> + <key>ID</key> + <integer>87</integer> + <key>Points</key> + <array> + <string>{350.25724339020428, 513.73666184812191}</string> + <string>{432.54926154576208, 492.26520134230651}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>58</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{429.24670918782579, 464.13918876647955}, {115.08661651611328, 28}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>86</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>1</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 <<interface>>\ +ResponseHandler}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>85</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>80</integer> + </dict> + <key>ID</key> + <integer>81</integer> + <key>Points</key> + <array> + <string>{388.98464357649044, 311.73041212162735}</string> + <string>{421.12154403577119, 242.99627753494852}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>52</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{298.33331664403278, 228.54334004720087}, {246, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>80</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 IdleStateAwareChannelUpstreamHandler}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>10</integer> + </dict> + <key>ID</key> + <integer>27</integer> + <key>Points</key> + <array> + <string>{129.27841644847507, 130.04334003445351}</string> + <string>{129.27841313680011, 80.666667938232422}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>69</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{71.445081075032547, 66.666667938232422}, {115.66666412353516, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>10</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>1</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 AbstractResource}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>78</integer> + </dict> + <key>ID</key> + <integer>79</integer> + <key>Points</key> + <array> + <string>{185.26276724928567, 130.48075966792658}</string> + <string>{297.8372493866778, 116.27917789513803}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>69</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{298.3333059188999, 92.666666666666913}, {151.17318725585938, 28}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>78</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 <<interface>>\ +ChannelPipelineFactory}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>77</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>75</integer> + </dict> + <key>ID</key> + <integer>76</integer> + <key>Points</key> + <array> + <string>{402.97819417613408, 311.99312587103537}</string> + <string>{465.1473485992222, 286.41782928501391}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>52</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{454.91339111328136, 258.22751967112259}, {89.419929504394531, 28}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>75</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 <<interface>>\ +Runnable}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>74</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>34</integer> + </dict> + <key>ID</key> + <integer>9</integer> + <key>Points</key> + <array> + <string>{143.15706944536032, 130.31230832708621}</string> + <string>{250.79491585172687, 74.231022194055214}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>Arrow</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>69</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{226.66667683919275, 45.999999999999929}, {102.88706970214844, 28}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>34</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>1</string> + <key>g</key> + <string>0</string> + <key>r</key> + <string>0</string> + </dict> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 <<interface>>\ +ServerProvider}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>33</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{344.6666666666668, 438.85001627604163}, {17, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>63</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 1}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Bounds</key> + <string>{{308.27841186523432, 496.00000000000023}, {29.333332061767578, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>62</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 1..*}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>58</integer> + </dict> + <key>ID</key> + <integer>61</integer> + <key>Points</key> + <array> + <string>{364.20702685721608, 438.63027009978003}</string> + <string>{326.6915789284576, 513.41623846111793}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>54</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{226.66666603088379, 513.86289469401004}, {192.55682373046875, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>58</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 RequestContext}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{226.66666603088379, 527.86289469401004}, {192.55682373046875, 42}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>59</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 HttpRequest request\ +ContentChannel requestContent\ +ContentChannel responseContent}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{226.66666603088379, 569.86289469401004}, {192.55682373046875, 28}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>60</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 connect()\ +handleResponse(Response)}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>GridH</key> + <array> + <integer>58</integer> + <integer>59</integer> + <integer>60</integer> + <array/> + </array> + <key>ID</key> + <integer>57</integer> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{226.66666666666663, 312.183349609375}, {317.66665649414062, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>52</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 ChannelContext}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{226.66666666666652, 326.183349609375}, {317.66665649414062, 28}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>53</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 LinkedBlockingQueue<ResponsePart> responseOutputs\ +Channel serverChannel}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{226.66666666666663, 354.183349609375}, {317.66665649414062, 84}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>54</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 channelConnected()\ +messageReceived()\ +exceptionCaught()\ +channelDisconnected()\ +channelIdle()\ +run()}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>GridH</key> + <array> + <integer>52</integer> + <integer>53</integer> + <integer>54</integer> + <array/> + </array> + <key>ID</key> + <integer>51</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{158.97158196265923, 262.43389980796718}, {29.333332061767578, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>47</integer> + <key>Line</key> + <dict> + <key>ID</key> + <integer>16</integer> + <key>Offset</key> + <real>12.666667938232422</real> + <key>Position</key> + <real>0.93361091613769531</real> + <key>RotationType</key> + <integer>0</integer> + </dict> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 0..*}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>104</integer> + </dict> + <key>ID</key> + <integer>16</integer> + <key>Points</key> + <array> + <string>{144.7864237041758, 242.98231480726633}</string> + <string>{163.77841186523429, 277.810302734375}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>HeadArrow</key> + <string>StickArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>71</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{33.000005086262888, 130.54334004720062}, {192.55682373046875, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>69</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 HttpServer}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{33.000005086262888, 144.54334004720062}, {192.55682373046875, 42}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>70</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 Channel serverChannel\ +Executor channelWorkerExecutor\ +HttpServerConfig config}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{33.000005086262888, 186.54334004720062}, {192.55682373046875, 56}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>Vertical</string> + <key>Flow</key> + <string>Resize</string> + <key>ID</key> + <integer>71</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>GradientCenter</key> + <string>{-0.29411799999999999, -0.264706}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 start()\ +close()\ +destroy()\ +getPipeline()}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>GridH</key> + <array> + <integer>69</integer> + <integer>70</integer> + <integer>71</integer> + <array/> + </array> + <key>ID</key> + <integer>68</integer> + </dict> + </array> + <key>GridInfo</key> + <dict/> + <key>GuidesLocked</key> + <string>NO</string> + <key>GuidesVisible</key> + <string>YES</string> + <key>HPages</key> + <integer>1</integer> + <key>ImageCounter</key> + <integer>1</integer> + <key>KeepToScale</key> + <false/> + <key>Layers</key> + <array> + <dict> + <key>Lock</key> + <string>NO</string> + <key>Name</key> + <string>Layer 1</string> + <key>Print</key> + <string>YES</string> + <key>View</key> + <string>YES</string> + </dict> + </array> + <key>LayoutInfo</key> + <dict> + <key>Animate</key> + <string>NO</string> + <key>circoMinDist</key> + <real>18</real> + <key>circoSeparation</key> + <real>0.0</real> + <key>layoutEngine</key> + <string>dot</string> + <key>neatoSeparation</key> + <real>0.0</real> + <key>twopiSeparation</key> + <real>0.0</real> + </dict> + <key>LinksVisible</key> + <string>NO</string> + <key>MagnetsVisible</key> + <string>NO</string> + <key>MasterSheets</key> + <array/> + <key>ModificationDate</key> + <string>2012-06-19 09:20:43 +0000</string> + <key>Modifier</key> + <string>Einar Rosenvinge</string> + <key>NotesVisible</key> + <string>NO</string> + <key>Orientation</key> + <integer>2</integer> + <key>OriginVisible</key> + <string>NO</string> + <key>PageBreaks</key> + <string>YES</string> + <key>PrintInfo</key> + <dict> + <key>NSBottomMargin</key> + <array> + <string>float</string> + <string>41</string> + </array> + <key>NSHorizonalPagination</key> + <array> + <string>coded</string> + <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string> + </array> + <key>NSLeftMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + <key>NSPaperSize</key> + <array> + <string>size</string> + <string>{594.99999713897705, 842}</string> + </array> + <key>NSPrintReverseOrientation</key> + <array> + <string>int</string> + <string>0</string> + </array> + <key>NSRightMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + <key>NSTopMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + </dict> + <key>PrintOnePage</key> + <false/> + <key>ReadOnly</key> + <string>NO</string> + <key>RowAlign</key> + <integer>1</integer> + <key>RowSpacing</key> + <real>36</real> + <key>SheetTitle</key> + <string>Canvas 1</string> + <key>SmartAlignmentGuidesActive</key> + <string>YES</string> + <key>SmartDistanceGuidesActive</key> + <string>YES</string> + <key>UniqueID</key> + <integer>1</integer> + <key>UseEntirePage</key> + <false/> + <key>VPages</key> + <integer>1</integer> + <key>WindowInfo</key> + <dict> + <key>CurrentSheet</key> + <integer>0</integer> + <key>ExpandedCanvases</key> + <array> + <dict> + <key>name</key> + <string>Canvas 1</string> + </dict> + </array> + <key>Frame</key> + <string>{{246, 375}, {1064, 803}}</string> + <key>ListView</key> + <true/> + <key>OutlineWidth</key> + <integer>142</integer> + <key>RightSidebar</key> + <false/> + <key>ShowRuler</key> + <true/> + <key>Sidebar</key> + <true/> + <key>SidebarWidth</key> + <integer>120</integer> + <key>VisibleRegion</key> + <string>{{-25, 0}, {610, 442.66666666666669}}</string> + <key>Zoom</key> + <real>1.5</real> + <key>ZoomValues</key> + <array> + <array> + <string>Canvas 1</string> + <real>1.5</real> + <real>0.25</real> + </array> + </array> + </dict> +</dict> +</plist> diff --git a/jdisc_http_service/docs/class-diagram.png b/jdisc_http_service/docs/class-diagram.png Binary files differnew file mode 100644 index 00000000000..ebccfd75bf9 --- /dev/null +++ b/jdisc_http_service/docs/class-diagram.png diff --git a/jdisc_http_service/docs/httpserver.html b/jdisc_http_service/docs/httpserver.html new file mode 100644 index 00000000000..66afeb687fe --- /dev/null +++ b/jdisc_http_service/docs/httpserver.html @@ -0,0 +1,96 @@ +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> + "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> + <title>HTTP Server Architecture</title> + <style type="text/css"> + body { + font: 13px/1.231 arial,helvetica,clean,sans-serif; + *font-size: small; + *font: x-small; + } + select,input,button,textarea { + font: 99% arial,helvetica,clean,sans-serif; + } + table{ + font-size: inherit; + font: 100%; + } + pre,code,kbd,samp,tt { + font-family: monospace; + *font-size: 108%; + line-height: 100%; + } + </style> +</head> +<body> +<p>The HTTP server is started by calling <code>HttpServer.start()</code>, which in turn calls <code>ServerBootstrap.bind()</code> + provided by + Netty.</p> + +<img src="class-diagram.png" alt="Class diagram"> + +<p>Since our HttpServer implements <code>ChannelPipelineFactory</code> (provided by jetty), its + <code>getPipeline()</code> method is called for every new channel that is connected. There is hence a one-to-many + relationship between a <code>HttpServer</code> and a pipeline (and a one-to-one relationship between an actual + channel and a pipeline).</p> + +<p>The pipeline is responsible for decoding (and possibly deflating etc.) every new request that is received on a + channel. The final element in the pipeline is a <code>ChannelContext</code>, which is the jDISC class for handling + requests on a channel.</p> + +<p>The <code>ChannelContext</code> implements <code>SimpleChannelUpstreamHandler</code> (provided by Jetty), which has + simple callback methods for various event types. <br/>Examples:</p> + +<ul> + <li><code>channelConnected()</code></li> + <li><code>channelDisconnected()</code></li> + <li><code>messageReceived()</code></li> +</ul> + +<p>Since <code>ChannelContext</code> supports HTTP keep-alive and HTTP pipelining, it needs to keep track of multiple + requests made on the channel, and their order.</p> + +<p>In <code>messageReceived()</code> it will:</p> +<ul> + <li>Determine if the element received is a new HTTP request, or a chunk belonging to the previous one.</li> + <li>If it's a request, create a DISC <code>Request</code> object for it, and call <code>Request.connect()</code>, + which will in turn give it to the actual application, through the use of + <code>RequestHandler.handleRequest()</code>. + </li> + <li>If it's a chunk, fetch the previously added <code>RequestContext</code>, and use it to write the data received + into the <code>ContentChannel</code>. + </li> +</ul> + +<p><code>RequestContext</code> keeps track of a request and its input and output <code>ContentChannel</code>s, and + related objects. Since <code>RequestContext</code> is a <code>ResponseHandler</code>, it is responsible for + instantiating and returning a <code>ContentChannel</code> when an application calls <code>handleResponse()</code>. + Two types are supported, one that supports HTTP response chunking, and one that does not. The type used is chosen + automatically based on HTTP version, headers etc.</p> + +<p>Since the jDISC API is fully asynchronous, operations can occur in any order. This is very extensively tested in the + HTTP server implementation. For instance, an application (<code>RequestHandler</code>) may choose to respond and + close the output <code>ContentChannel</code> immediately upon receiving the request, before the body of the request + has been written into the input <code>ContentChannel</code> of the <code>RequestHandler</code>. All such cases are + tested and properly handled.</p> + +<p>As one can see from the illustration, <code>ChannelContext</code> is also a <code>Runnable</code>, i.e. it keeps one + thread per channel. The HTTP server has two modes of operation, <code>optimizeForPipeline</code> <code>true</code> + or <code>false</code> in <code>HttpServerConfig</code>.</p> + +<p>If <code>optimizeForPipeline</code> is set to <code>true</code>, response chunks are enqueued on a blocking queue in + <code>ChannelContext</code> when <code>ContentChannel.write()</code> is called. The <code>ChannelContext</code> + thread is responsible for actually writing them, and closing the channel when appropriate. Since the HTTP server + supports pipelining, and writes from an application may occur in any order, special care is taken to write response + chunks in the correct order.</p> + +<p>If <code>optimizeForPipeline</code> is set to <code>false</code>, a call to <code>ContentChannel.write()</code> will + lead to an actual write on the wire, iff. the given chunk to be written is the next in line. Otherwise this is a + no-op. This also means that a <code>ContentChannel.write()</code> may lead to a cascade of writes that have been + enqueued since they were out-of-order when their <code>write()</code> was called. The <code>ChannelContext</code> + thread still takes care of channel closing in most cases.</p> +</body> +</html> diff --git a/jdisc_http_service/pom.xml b/jdisc_http_service/pom.xml new file mode 100644 index 00000000000..15518d20c62 --- /dev/null +++ b/jdisc_http_service/pom.xml @@ -0,0 +1,210 @@ +<?xml version="1.0"?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>6-SNAPSHOT</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + <artifactId>jdisc_http_service</artifactId> + <version>6-SNAPSHOT</version> + <packaging>container-plugin</packaging> + <name>${project.artifactId}</name> + <dependencies> + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + <scope>provided</scope> + <classifier>no_aop</classifier> + </dependency> + <dependency> + <groupId>com.ning</groupId> + <artifactId>async-http-client</artifactId> + <exclusions> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>commons-pool</groupId> + <artifactId>commons-pool</artifactId> + </dependency> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpmime</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.cthul</groupId> + <artifactId>cthul-matchers</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>jdisc_jetty</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.grizzly</groupId> + <artifactId>grizzly-websockets</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-lib</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>defaults</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>jdisc_core</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>annotations</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>component</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>container-accesslogging</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespajlib</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jetbrains</groupId> + <artifactId>annotations</artifactId> + <version>13.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-test</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <testNGArtifactName>org.testng:testng</testNGArtifactName> + <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile> + </configuration> + <executions> + <execution> + <id>default-test</id> + <phase>test</phase> + <goals> + <goal>test</goal> + </goals> + <configuration> + <testNGArtifactName>org.testng:testng</testNGArtifactName> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <compilerArgs> + <arg>-Xlint:rawtypes</arg> + <arg>-Xlint:unchecked</arg> + <arg>-Xlint:deprecation</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <discPreInstallBundle> + asm-debug-all-${asm-debug-all.version}.jar, + javax.servlet-api-3.1.0.jar, + jetty-continuation-${jetty.version}.jar, + jetty-http-${jetty.version}.jar, + jetty-io-${jetty.version}.jar, + jetty-security-${jetty.version}.jar, + jetty-server-${jetty.version}.jar, + jetty-servlet-${jetty.version}.jar, + jetty-servlets-${jetty.version}.jar, + jetty-util-${jetty.version}.jar, + org.apache.aries.spifly.dynamic.bundle-${aries.spifly.version}.jar, + org.apache.aries.util-${aries.util.version}.jar, + websocket-api-${jetty.version}.jar, + websocket-client-${jetty.version}.jar, + websocket-common-${jetty.version}.jar, + websocket-server-${jetty.version}.jar, + websocket-servlet-${jetty.version}.jar, + component-jar-with-dependencies.jar + </discPreInstallBundle> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java new file mode 100644 index 00000000000..156215cf22b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +/** + * A store of certificates. An implementation can be plugged in to provide certificates to components who use it. + * + * @author bratseth + */ +public interface CertificateStore { + + /** Returns a certificate for a given appid, using the default TTL and retry time */ + default String getCertificate(String appid) { return getCertificate(appid, 0L, 0L); } + + /** Returns a certificate for a given appid, using a TTL and default retry time */ + default String getCertificate(String appid, long ttl) { return getCertificate(appid, ttl, 0L); } + + /** + * Returns a certificate for a given appid, using a TTL and default retry time + * + * @param ttl certificate TTL in ms. Use the default TTL if set to 0 + * @param retry if no certificate is found, allow access to cert DB again in + * "retry" ms. Use the default retry time if set to 0. + */ + String getCertificate(String appid, long ttl, long retry); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java new file mode 100644 index 00000000000..874bf35021b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java @@ -0,0 +1,297 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import org.jboss.netty.handler.codec.http.CookieDecoder; +import org.jboss.netty.handler.codec.http.CookieEncoder; +import org.jboss.netty.handler.codec.http.DefaultCookie; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class Cookie { + + private final Set<Integer> ports = new HashSet<>(); + private String name; + private String value; + private String domain; + private String path; + private String comment; + private String commentUrl; + private long maxAgeMillis = TimeUnit.SECONDS.toMillis(Integer.MIN_VALUE); + private int version; + private boolean secure; + private boolean httpOnly; + private boolean discard; + + public Cookie() { + } + + public Cookie(Cookie cookie) { + ports.addAll(cookie.ports); + name = cookie.name; + value = cookie.value; + domain = cookie.domain; + path = cookie.path; + comment = cookie.comment; + commentUrl = cookie.commentUrl; + maxAgeMillis = cookie.maxAgeMillis; + version = cookie.version; + secure = cookie.secure; + httpOnly = cookie.httpOnly; + discard = cookie.discard; + } + + public Cookie(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public Cookie setName(String name) { + this.name = name; + return this; + } + + public String getValue() { + return value; + } + + public Cookie setValue(String value) { + this.value = value; + return this; + } + + public String getDomain() { + return domain; + } + + public Cookie setDomain(String domain) { + this.domain = domain; + return this; + } + + public String getPath() { + return path; + } + + public Cookie setPath(String path) { + this.path = path; + return this; + } + + public String getComment() { + return comment; + } + + public Cookie setComment(String comment) { + this.comment = comment; + return this; + } + + public String getCommentURL() { + return getCommentUrl(); + } + + public Cookie setCommentURL(String commentUrl) { + return setCommentUrl(commentUrl); + } + + public String getCommentUrl() { + return commentUrl; + } + + public Cookie setCommentUrl(String commentUrl) { + this.commentUrl = commentUrl; + return this; + } + + public int getMaxAge(TimeUnit unit) { + return (int)unit.convert(maxAgeMillis, TimeUnit.MILLISECONDS); + } + + public Cookie setMaxAge(int maxAge, TimeUnit unit) { + this.maxAgeMillis = unit.toMillis(maxAge); + return this; + } + + public int getVersion() { + return version; + } + + public Cookie setVersion(int version) { + this.version = version; + return this; + } + + public boolean isSecure() { + return secure; + } + + public Cookie setSecure(boolean secure) { + this.secure = secure; + return this; + } + + public boolean isHttpOnly() { + return httpOnly; + } + + public Cookie setHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + public boolean isDiscard() { + return discard; + } + + public Cookie setDiscard(boolean discard) { + this.discard = discard; + return this; + } + + public Set<Integer> ports() { + return ports; + } + + @Override + public int hashCode() { + return ports.hashCode() + hashCode(name) + hashCode(value) + hashCode(domain) + hashCode(path) + + hashCode(comment) + hashCode(commentUrl) + Long.valueOf(maxAgeMillis).hashCode() + + Integer.valueOf(version).hashCode() + Boolean.valueOf(secure).hashCode() + + Boolean.valueOf(httpOnly).hashCode() + Boolean.valueOf(discard).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Cookie)) { + return false; + } + Cookie rhs = (Cookie)obj; + if (!ports.equals(rhs.ports)) { + return false; + } + if (!equals(name, rhs.name)) { + return false; + } + if (!equals(value, rhs.value)) { + return false; + } + if (!equals(domain, rhs.domain)) { + return false; + } + if (!equals(path, rhs.path)) { + return false; + } + if (!equals(comment, rhs.comment)) { + return false; + } + if (!equals(commentUrl, rhs.commentUrl)) { + return false; + } + if (maxAgeMillis != rhs.maxAgeMillis) { + return false; + } + if (version != rhs.version) { + return false; + } + if (secure != rhs.secure) { + return false; + } + if (httpOnly != rhs.httpOnly) { + return false; + } + if (discard != rhs.discard) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + ret.append(name).append("=").append(value); + return ret.toString(); + } + + public static String toCookieHeader(Iterable<? extends Cookie> cookies) { + return encodeCookies(cookies, false); + } + + public static List<Cookie> fromCookieHeader(String headerVal) { + return decodeCookies(headerVal); + } + + public static String toSetCookieHeader(Iterable<? extends Cookie> cookies) { + return encodeCookies(cookies, true); + } + + public static List<Cookie> fromSetCookieHeader(String headerVal) { + return decodeCookies(headerVal); + } + + private static String encodeCookies(Iterable<? extends Cookie> cookies, boolean server) { + CookieEncoder encoder = new org.jboss.netty.handler.codec.http.CookieEncoder(server); + for (Cookie cookie : cookies) { + org.jboss.netty.handler.codec.http.Cookie nettyCookie = + new DefaultCookie(String.valueOf(cookie.getName()), String.valueOf(cookie.getValue())); + nettyCookie.setComment(cookie.getComment()); + nettyCookie.setCommentUrl(cookie.getCommentUrl()); + nettyCookie.setDiscard(cookie.isDiscard()); + nettyCookie.setDomain(cookie.getDomain()); + nettyCookie.setHttpOnly(cookie.isHttpOnly()); + nettyCookie.setMaxAge(cookie.getMaxAge(TimeUnit.SECONDS)); + nettyCookie.setPath(cookie.getPath()); + nettyCookie.setSecure(cookie.isSecure()); + nettyCookie.setVersion(cookie.getVersion()); + nettyCookie.setPorts(cookie.ports()); + encoder.addCookie(nettyCookie); + } + return encoder.encode(); + } + + private static List<Cookie> decodeCookies(String str) { + CookieDecoder decoder = new CookieDecoder(); + List<Cookie> ret = new LinkedList<>(); + for (org.jboss.netty.handler.codec.http.Cookie nettyCookie : decoder.decode(str)) { + Cookie cookie = new Cookie(); + cookie.setName(nettyCookie.getName()); + cookie.setValue(nettyCookie.getValue()); + cookie.setComment(nettyCookie.getComment()); + cookie.setCommentUrl(nettyCookie.getCommentUrl()); + cookie.setDiscard(nettyCookie.isDiscard()); + cookie.setDomain(nettyCookie.getDomain()); + cookie.setHttpOnly(nettyCookie.isHttpOnly()); + cookie.setMaxAge(nettyCookie.getMaxAge(), TimeUnit.SECONDS); + cookie.setPath(nettyCookie.getPath()); + cookie.setSecure(nettyCookie.isSecure()); + cookie.setVersion(nettyCookie.getVersion()); + cookie.ports().addAll(nettyCookie.getPorts()); + ret.add(cookie); + } + return ret; + } + + private static int hashCode(Object obj) { + if (obj == null) { + return 0; + } + return obj.hashCode(); + } + + private static boolean equals(Object lhs, Object rhs) { + if (lhs == null || rhs == null) { + return lhs == rhs; + } + return lhs.equals(rhs); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java new file mode 100644 index 00000000000..0cc13394f99 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java @@ -0,0 +1,124 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +/** + * @author <a href="mailto:anirudha@yahoo-inc.com">Anirudha Khanna</a> + */ +@SuppressWarnings("UnusedDeclaration") +public class HttpHeaders { + + public static final class Names { + + public static final String ACCEPT = "Accept"; + public static final String ACCEPT_CHARSET = "Accept-Charset"; + public static final String ACCEPT_ENCODING = "Accept-Encoding"; + public static final String ACCEPT_LANGUAGE = "Accept-Language"; + public static final String ACCEPT_RANGES = "Accept-Ranges"; + public static final String ACCEPT_PATCH = "Accept-Patch"; + public static final String AGE = "Age"; + public static final String ALLOW = "Allow"; + public static final String AUTHORIZATION = "Authorization"; + public static final String CACHE_CONTROL = "Cache-Control"; + public static final String CONNECTION = "Connection"; + public static final String CONTENT_BASE = "Content-Base"; + public static final String CONTENT_ENCODING = "Content-Encoding"; + public static final String CONTENT_LANGUAGE = "Content-Language"; + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String CONTENT_LOCATION = "Content-Location"; + public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; + public static final String CONTENT_MD5 = "Content-MD5"; + public static final String CONTENT_RANGE = "Content-Range"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String COOKIE = "Cookie"; + public static final String DATE = "Date"; + public static final String ETAG = "ETag"; + public static final String EXPECT = "Expect"; + public static final String EXPIRES = "Expires"; + public static final String FROM = "From"; + public static final String HOST = "Host"; + public static final String IF_MATCH = "If-Match"; + public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String IF_NONE_MATCH = "If-None-Match"; + public static final String IF_RANGE = "If-Range"; + public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + public static final String LAST_MODIFIED = "Last-Modified"; + public static final String LOCATION = "Location"; + public static final String MAX_FORWARDS = "Max-Forwards"; + public static final String ORIGIN = "Origin"; + public static final String PRAGMA = "Pragma"; + public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; + public static final String RANGE = "Range"; + public static final String REFERER = "Referer"; + public static final String RETRY_AFTER = "Retry-After"; + public static final String SEC_WEBSOCKET_KEY1 = "Sec-WebSocket-Key1"; + public static final String SEC_WEBSOCKET_KEY2 = "Sec-WebSocket-Key2"; + public static final String SEC_WEBSOCKET_LOCATION = "Sec-WebSocket-Location"; + public static final String SEC_WEBSOCKET_ORIGIN = "Sec-WebSocket-Origin"; + public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; + public static final String SERVER = "Server"; + public static final String SET_COOKIE = "Set-Cookie"; + public static final String SET_COOKIE2 = "Set-Cookie2"; + public static final String TE = "TE"; + public static final String TRAILER = "Trailer"; + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; + public static final String UPGRADE = "Upgrade"; + public static final String USER_AGENT = "User-Agent"; + public static final String VARY = "Vary"; + public static final String VIA = "Via"; + public static final String WARNING = "Warning"; + public static final String WEBSOCKET_LOCATION = "WebSocket-Location"; + public static final String WEBSOCKET_ORIGIN = "WebSocket-Origin"; + public static final String WEBSOCKET_PROTOCOL = "WebSocket-Protocol"; + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + public static final String X_DISABLE_CHUNKING = "X-JDisc-Disable-Chunking"; + public static final String X_ENABLE_TRACE_ID = "X-JDisc-Enable-TraceId"; + public static final String X_TRACE_ID = "X-JDisc-TraceId"; + public static final String X_YAHOO_SERVING_HOST = "X-Yahoo-Serving-Host"; + + private Names() { + // hide + } + } + + public static final class Values { + + public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; + public static final String BASE64 = "base64"; + public static final String BINARY = "binary"; + public static final String BYTES = "bytes"; + public static final String CHARSET = "charset"; + public static final String CHUNKED = "chunked"; + public static final String CLOSE = "close"; + public static final String COMPRESS = "compress"; + public static final String CONTINUE = "100-continue"; + public static final String DEFLATE = "deflate"; + public static final String GZIP = "gzip"; + public static final String IDENTITY = "identity"; + public static final String KEEP_ALIVE = "keep-alive"; + public static final String MAX_AGE = "max-age"; + public static final String MAX_STALE = "max-stale"; + public static final String MIN_FRESH = "min-fresh"; + public static final String MUST_REVALIDATE = "must-revalidate"; + public static final String NO_CACHE = "no-cache"; + public static final String NO_STORE = "no-store"; + public static final String NO_TRANSFORM = "no-transform"; + public static final String NONE = "none"; + public static final String ONLY_IF_CACHED = "only-if-cached"; + public static final String PRIVATE = "private"; + public static final String PROXY_REVALIDATE = "proxy-revalidate"; + public static final String PUBLIC = "public"; + public static final String QUOTED_PRINTABLE = "quoted-printable"; + public static final String S_MAXAGE = "s-maxage"; + public static final String TRAILERS = "trailers"; + public static final String UPGRADE = "Upgrade"; + public static final String WEBSOCKET = "WebSocket"; + + private Values() { + // hide + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java new file mode 100644 index 00000000000..580f83ca5a8 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java @@ -0,0 +1,316 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; +import org.jboss.netty.handler.codec.http.QueryStringDecoder; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * A HTTP request. + * + * @author <a href="mailto:anirudha@yahoo-inc.com">Anirudha Khanna</a> + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class HttpRequest extends Request implements ServletOrJdiscHttpRequest { + + public enum Method { + OPTIONS, + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + TRACE, + CONNECT + } + + public enum Version { + HTTP_1_0("HTTP/1.0"), + HTTP_1_1("HTTP/1.1"); + + private final String str; + + private Version(String str) { + this.str = str; + } + + @Override + public String toString() { + return str; + } + + public static Version fromString(String str) { + for (Version version : values()) { + if (version.str.equals(str)) { + return version; + } + } + throw new IllegalArgumentException(str); + } + } + + private final HeaderFields trailers = new HeaderFields(); + private final Map<String, List<String>> parameters = new HashMap<>(); + private final long connectedAt; + private Method method; + private Version version; + private SocketAddress remoteAddress; + private URI proxyServer; + private Long connectionTimeout; + + protected HttpRequest(CurrentContainer container, URI uri, Method method, Version version, + SocketAddress remoteAddress, Long connectedAtMillis) + { + super(container, uri); + try { + this.method = method; + this.version = version; + this.remoteAddress = remoteAddress; + this.parameters.putAll(new QueryStringDecoder(uri.toString(), true).getParameters()); + if (connectedAtMillis != null) { + this.connectedAt = connectedAtMillis; + } else { + this.connectedAt = creationTime(TimeUnit.MILLISECONDS); + } + } catch (RuntimeException e) { + release(); + throw e; + } + } + + private HttpRequest(Request parent, URI uri, Method method, Version version) { + super(parent, uri); + try { + this.method = method; + this.version = version; + this.remoteAddress = null; + this.parameters.putAll(new QueryStringDecoder(uri.toString(), true).getParameters()); + this.connectedAt = creationTime(TimeUnit.MILLISECONDS); + } catch (RuntimeException e) { + release(); + throw e; + } + } + + public Method getMethod() { + return method; + } + + public void setMethod(Method method) { + this.method = method; + } + + public Version getVersion() { + return version; + } + + @Override + public String getRemoteHostAddress() { + if (remoteAddress instanceof InetSocketAddress) + return ((InetSocketAddress) remoteAddress).getAddress().getHostAddress(); + else + throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName()); + } + + @Override + public String getRemoteHostName() { + if (remoteAddress instanceof InetSocketAddress) { + InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress(); + if (remoteInetAddress == null) return null; // not resolved; we have no network + return remoteInetAddress.getHostName(); + } + else { + throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName()); + } + } + + @Override + public int getRemotePort() { + if (remoteAddress instanceof InetSocketAddress) + return ((InetSocketAddress) remoteAddress).getPort(); + else + throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName()); + } + + public void setVersion(Version version) { + this.version = version; + } + + public SocketAddress getRemoteAddress() { + return remoteAddress; + } + + public void setRemoteAddress(SocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + + public URI getProxyServer() { + return proxyServer; + } + + public void setProxyServer(URI proxyServer) { + this.proxyServer = proxyServer; + } + + /** + * <p>For server requests, this returns the timestamp of when the underlying HTTP channel was connected. + * This is whatever value was returned by {@link + * com.yahoo.jdisc.Timer#currentTimeMillis()} at the time.</p> + * + * <p>For client requests, this returns the same value as {@link #creationTime(java.util.concurrent.TimeUnit)}.</p> + * + * @param unit the unit to return the time in + * @return the timestamp of when the underlying HTTP channel was connected, or request creation time + */ + public long getConnectedAt(TimeUnit unit) { + return unit.convert(connectedAt, TimeUnit.MILLISECONDS); + } + + public Long getConnectionTimeout(TimeUnit unit) { + if (connectionTimeout == null) { + return null; + } + return unit.convert(connectionTimeout, TimeUnit.MILLISECONDS); + } + + /** + * <p>Sets the allocated time that this HttpRequest is allowed to spend trying to connect to a remote host. This has + * no effect on an HttpRequest received by a {@link RequestHandler}. If no connection timeout is assigned to an + * HttpRequest, it defaults the connection-timeout in the corresponding {@link + * com.yahoo.jdisc.http.client.HttpClientConfig}.</p> + * + * <p><b>NOTE:</b> Where {@link Request#setTimeout(long, TimeUnit)} sets the expiration time between calling a + * RequestHandler and a {@link ResponseHandler}, this method sets the expiration time of the connect-operation as + * performed by the {@link com.yahoo.jdisc.http.client.HttpClient}.</p> + * + * @param timeout The allocated amount of time. + * @param unit The time unit of the <em>timeout</em> argument. + */ + public void setConnectionTimeout(long timeout, TimeUnit unit) { + this.connectionTimeout = unit.toMillis(timeout); + } + + public Map<String, List<String>> parameters() { + return parameters; + } + + @Override + public void copyHeaders(HeaderFields target) { + target.addAll(headers()); + } + + public List<Cookie> decodeCookieHeader() { + List<String> cookies = headers().get(HttpHeaders.Names.COOKIE); + if (cookies == null) { + return Collections.emptyList(); + } + List<Cookie> ret = new LinkedList<>(); + for (String cookie : cookies) { + ret.addAll(Cookie.fromCookieHeader(cookie)); + } + return ret; + } + + public void encodeCookieHeader(List<Cookie> cookies) { + headers().put(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies)); + } + + /** + * <p>Returns the set of trailer header fields of this HttpRequest. These are typically meta-data that should have + * been part of {@link #headers()}, but were not available prior to calling {@link #connect(ResponseHandler)}. You + * must NOT WRITE to these headers AFTER calling {@link ContentChannel#close(CompletionHandler)}, and you must NOT + * READ from these headers BEFORE {@link ContentChannel#close(CompletionHandler)} has been called.</p> + * + * <p><b>NOTE:</b> These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to + * prevent concurrency issues such as ConcurrentModificationExceptions.</p> + * + * @return The trailer headers of this HttpRequest. + */ + public HeaderFields trailers() { + return trailers; + } + + /** + * Returns whether this request was <em>explicitly</em> chunked from the client. NOTE that there are cases + * where the underlying HTTP server library (Netty for the time being) will read the request in a chunked manner. An + * application MUST wait for {@link com.yahoo.jdisc.handler.ContentChannel#close(com.yahoo.jdisc.handler.CompletionHandler)} + * before it can actually know that it has received the entire request. + * + * @return true if this request was chunked from the client. + */ + public boolean isChunked() { + return version == Version.HTTP_1_1 && + headers().containsIgnoreCase(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); + } + + public boolean hasChunkedResponse() { + return version == Version.HTTP_1_1 && + !headers().isTrue(HttpHeaders.Names.X_DISABLE_CHUNKING); + } + + public boolean isKeepAlive() { + if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)) { + return true; + } + if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE)) { + return false; + } + return version == Version.HTTP_1_1; + } + + public static HttpRequest newServerRequest(CurrentContainer container, URI uri) { + return newServerRequest(container, uri, Method.GET); + } + + public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method) { + return newServerRequest(container, uri, method, Version.HTTP_1_1); + } + + public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version) { + return newServerRequest(container, uri, method, version, null); + } + + @SuppressWarnings("deprecation") + public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version, + SocketAddress remoteAddress) { + return new HttpRequest(container, uri, method, version, remoteAddress, null); + } + + public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version, + SocketAddress remoteAddress, long connectedAtMillis) + { + return new HttpRequest(container, uri, method, version, remoteAddress, connectedAtMillis); + } + + public static HttpRequest newClientRequest(Request parent, URI uri) { + return newClientRequest(parent, uri, Method.GET); + } + + public static HttpRequest newClientRequest(Request parent, URI uri, Method method) { + return newClientRequest(parent, uri, method, Version.HTTP_1_1); + } + + @SuppressWarnings("deprecation") + public static HttpRequest newClientRequest(Request parent, URI uri, Method method, Version version) { + return new HttpRequest(parent, uri, method, version); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java new file mode 100644 index 00000000000..5cd8dec0af9 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java @@ -0,0 +1,130 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse; +import edu.umd.cs.findbugs.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * A HTTP response. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class HttpResponse extends Response implements ServletOrJdiscHttpResponse { + + private final HeaderFields trailers = new HeaderFields(); + private final StringBuffer accessLogExtra = new StringBuffer(); + private boolean chunkedEncodingEnabled = true; + private String message; + private final Request request; + + public interface Status extends Response.Status { + + int REQUEST_ENTITY_TOO_LARGE = REQUEST_TOO_LONG; + int REQUEST_RANGE_NOT_SATISFIABLE = REQUESTED_RANGE_NOT_SATISFIABLE; + } + + protected HttpResponse(Request request, int status, String message, Throwable error) { + super(status, error); + this.message = message; + this.request = request; + } + + public boolean isChunkedEncodingEnabled() { + if (headers().contains(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED)) { + return true; + } + if (headers().containsKey(HttpHeaders.Names.CONTENT_LENGTH)) { + return false; + } + return chunkedEncodingEnabled; + } + + public void setChunkedEncodingEnabled(boolean chunkedEncodingEnabled) { + this.chunkedEncodingEnabled = chunkedEncodingEnabled; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + @Override + public void copyHeaders(HeaderFields target) { + target.addAll(headers()); + } + + public List<Cookie> decodeSetCookieHeader() { + List<String> cookies = headers().get(HttpHeaders.Names.SET_COOKIE); + if (cookies == null) { + return Collections.emptyList(); + } + List<Cookie> ret = new LinkedList<>(); + for (String cookie : cookies) { + ret.addAll(Cookie.fromSetCookieHeader(cookie)); + } + return ret; + } + + public void encodeSetCookieHeader(List<Cookie> cookies) { + headers().remove(HttpHeaders.Names.SET_COOKIE); + for (Cookie cookie : cookies) { + headers().add(HttpHeaders.Names.SET_COOKIE, Cookie.toSetCookieHeader(Arrays.asList(cookie))); + } + } + + /** + * <p>Returns the set of trailer header fields of this HttpResponse. These are typically meta-data that should have + * been part of {@link #headers()}, but were not available prior to calling {@link + * ResponseHandler#handleResponse(Response)}. You must NOT WRITE to these headers AFTER calling {@link + * ContentChannel#close(CompletionHandler)}, and you must NOT READ from these headers BEFORE {@link + * ContentChannel#close(CompletionHandler)} has been called.</p> + * + * <p><b>NOTE:</b> These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to + * prevent concurrency issues such as ConcurrentModificationExceptions.</p> + * + * @return The trailer headers of this HttpRequest. + */ + public HeaderFields trailers() { + return trailers; + } + + public static boolean isServerError(Response response) { + return (response.getStatus() >= 500) && (response.getStatus() < 600); + } + + public static HttpResponse newInstance(int status) { + return new HttpResponse(null, status, null, null); + } + + public static HttpResponse newInstance(int status, String message) { + return new HttpResponse(null, status, message, null); + } + + public static HttpResponse newError(Request request, int status, Throwable error) { + return new HttpResponse(request, status, formatMessage(error), error); + } + + public static HttpResponse newInternalServerError(Request request, Throwable error) { + return new HttpResponse(request, Status.INTERNAL_SERVER_ERROR, formatMessage(error), error); + } + + private static String formatMessage(Throwable t) { + String msg = t.getMessage(); + return msg != null ? msg : t.toString(); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java new file mode 100644 index 00000000000..a3ef08df486 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +/** + * An abstraction of a secret store for e.g passwords. + * Implementations can be plugged in to provide passwords for various keys. + * + * @author bratseth + */ +public interface SecretStore { + + /** Returns the secret for this key */ + String getSecret(String key); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java new file mode 100644 index 00000000000..8a22b67b297 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.google.common.annotations.Beta; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.net.SocketAddress; +import java.net.URI; + +/** + * Represents a WebSocket request. + * + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +@Beta +public class WebSocketRequest extends HttpRequest { + + @SuppressWarnings("deprecation") + protected WebSocketRequest(CurrentContainer current, URI uri, Method method, Version version, + SocketAddress remoteAddress) { + super(current, uri, method, version, remoteAddress, null); + } + + public static WebSocketRequest newServerRequest(CurrentContainer current, URI uri, Method method, Version version, + SocketAddress remoteAddress) { + return new WebSocketRequest(current, uri, method, version, remoteAddress); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java new file mode 100644 index 00000000000..19f65633419 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java @@ -0,0 +1,187 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHandler; +import com.ning.http.client.FluentCaseInsensitiveStringsMap; +import com.ning.http.client.HttpResponseBodyPart; +import com.ning.http.client.HttpResponseHeaders; +import com.ning.http.client.HttpResponseStatus; +import com.ning.http.client.Response; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Timer; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpResponse; + +import java.net.ConnectException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + * @since 2.0 + */ +class AsyncResponseHandler implements AsyncHandler<Response> { + + private final CompletionHandler abortionHandler = new AbortionHandler(); + private final Request request; + private final ResponseHandler responseHandler; + private final Metric metric; + private final Metric.Context metricCtx; + private final Timer timer; + private int statusCode; + private String statusText; + private ContentChannel content; + private boolean aborted = false; + private long requestCreationTime; + private long transferStartTime; + + public AsyncResponseHandler(Request request, ResponseHandler responseHandler, Metric metric, + Metric.Context metricCtx) + { + this.request = request; + this.responseHandler = responseHandler; + this.metric = metric; + this.metricCtx = metricCtx; + this.timer = request.container().getInstance(Timer.class); + metric.add(HttpClient.Metrics.NUM_REQUESTS, 1, metricCtx); + this.requestCreationTime = timer.currentTimeMillis(); + } + + @Override + public void onThrowable(Throwable t) { + abort(t); + } + + @Override + public STATE onStatusReceived(HttpResponseStatus status) throws Exception { + if (aborted) { + return STATE.ABORT; + } + long latency = timer.currentTimeMillis() - request.creationTime(TimeUnit.MILLISECONDS); + metric.set(HttpClient.Metrics.REQUEST_LATENCY, latency, metricCtx); + metric.add(HttpClient.Metrics.NUM_RESPONSES, 1, metricCtx); + statusCode = status.getStatusCode(); + statusText = status.getStatusText(); + + metric.add(HttpClient.Metrics.NUM_BYTES_RECEIVED, ((Integer.SIZE)/8) + statusText.getBytes().length, metricCtx); // status code is an integer + return STATE.CONTINUE; + } + + @Override + public STATE onHeadersReceived(HttpResponseHeaders headers) throws Exception { + this.transferStartTime = timer.currentTimeMillis(); + + if (aborted) { + return STATE.ABORT; + } + HttpResponse response = HttpResponse.newInstance(statusCode, statusText); + + FluentCaseInsensitiveStringsMap headerMap = headers.getHeaders(); + response.headers().addAll(headerMap); + content = responseHandler.handleResponse(response); + + metric.add(HttpClient.Metrics.NUM_BYTES_RECEIVED, headerMap.size(), metricCtx); + + return STATE.CONTINUE; + } + + @Override + public STATE onBodyPartReceived(HttpResponseBodyPart part) throws Exception { + if (aborted) { + return STATE.ABORT; + } + metric.add(HttpClient.Metrics.NUM_BYTES_RECEIVED, part.getBodyPartBytes().length, metricCtx); + + content.write(part.getBodyByteBuffer(), abortionHandler); + return STATE.CONTINUE; + } + + @Override + public Response onCompleted() throws Exception { + long now = timer.currentTimeMillis(); + metric.set(HttpClient.Metrics.TRANSFER_LATENCY, now - transferStartTime, metricCtx); + metric.set(HttpClient.Metrics.TOTAL_LATENCY, now - requestCreationTime, metricCtx); + + if (aborted) { + return null; + } + content.close(abortionHandler); + return EmptyResponse.INSTANCE; + } + + /** + * Returns the original request associated with this handler. Note: It is the caller's responsibility to ensure + * that the request is properly retained and released. + */ + public Request getRequest() { + return request; + } + + private void abort(Throwable t) { + if (aborted) { + return; + } + aborted = true; + updateErrorMetric(t); + if (content == null) { + dispatchErrorResponse(t); + } + if (content != null) { + terminateContent(); + } + } + + private void updateErrorMetric(Throwable t) { + try { + if (t instanceof ConnectException) { + metric.add(HttpClient.Metrics.CONNECTION_EXCEPTIONS, 1, metricCtx); + } else if (t instanceof TimeoutException) { + metric.add(HttpClient.Metrics.TIMEOUT_EXCEPTIONS, 1, metricCtx); + } else { + metric.add(HttpClient.Metrics.OTHER_EXCEPTIONS, 1, metricCtx); + } + } catch (Exception e) { + // ignore + } + } + + private void dispatchErrorResponse(Throwable t) { + int status; + if (t instanceof ConnectException) { + status = com.yahoo.jdisc.Response.Status.SERVICE_UNAVAILABLE; + } else if (t instanceof TimeoutException) { + status = com.yahoo.jdisc.Response.Status.REQUEST_TIMEOUT; + } else { + status = com.yahoo.jdisc.Response.Status.BAD_REQUEST; + } + try { + content = responseHandler.handleResponse(HttpResponse.newError(request, status, t)); + } catch (Exception e) { + // ignore + } + } + + private void terminateContent() { + try { + content.close(null); + } catch (Exception e) { + // ignore + } + } + + private class AbortionHandler implements CompletionHandler { + + @Override + public void completed() { + + } + + @Override + public void failed(Throwable t) { + abort(t); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java new file mode 100644 index 00000000000..4b4b48dd05b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +final class BufferedRequest { + + private BufferedRequest() { + // hide + } + + public static ContentChannel executeRequest(AsyncHttpClient ningClient, Request request, HttpRequest.Method method, + ResponseHandler handler, Metric metric, Metric.Context ctx) + { + return new BufferedRequestContent(ningClient, request, method, + new AsyncResponseHandler(request, handler, metric, ctx)); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java new file mode 100644 index 00000000000..c09dcef98f4 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java @@ -0,0 +1,99 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.RequestBuilder; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.core.HeaderFieldsUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + * @since 2.0 + */ +class BufferedRequestContent implements ContentChannel { + + private final AsyncHttpClient client; + private final AsyncResponseHandler handler; + private final Request request; + private final HttpRequest.Method method; + private final List<CompletionHandler> writeCompletions = new LinkedList<>(); + private final Object contentLock = new Object(); + private ByteArrayOutputStream content = new ByteArrayOutputStream(); + + public BufferedRequestContent(AsyncHttpClient client, Request request, HttpRequest.Method method, + AsyncResponseHandler handler) { + this.client = client; + this.request = request; + this.method = method; + this.handler = handler; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler writeCompletion) { + Objects.requireNonNull(buf, "buf"); + synchronized (contentLock) { + if (content == null) { + throw new IllegalStateException("ContentChannel closed."); + } + for (int i = 0, len = buf.remaining(); i < len; ++i) { + content.write(buf.get()); + } + if (writeCompletion != null) { + writeCompletions.add(writeCompletion); + } + } + } + + @Override + public void close(CompletionHandler closeCompletion) { + byte[] content; + synchronized (contentLock) { + content = this.content.toByteArray(); + this.content = null; + } + try { + executeRequest(content); + for (CompletionHandler writeCompletion : writeCompletions) { + writeCompletion.completed(); + } + if (closeCompletion != null) { + closeCompletion.completed(); + } + } catch (Exception e) { + for (CompletionHandler writeCompletion : writeCompletions) { + tryFail(writeCompletion, e); + } + if (closeCompletion != null) { + tryFail(closeCompletion, e); + } + } + } + + private void tryFail(CompletionHandler handler, Throwable t) { + try { + handler.failed(t); + } catch (Exception e) { + // ignore + } + } + + private void executeRequest(final byte[] body) throws IOException { + RequestBuilder builder = RequestBuilderFactory.newInstance(request, method); + HeaderFieldsUtil.copyTrailers(request, builder); + if (body.length > 0) { + builder.setContentLength(body.length); + builder.setBody(body); + } + client.executeRequest(builder.build(), handler); + } +}
\ No newline at end of file diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java new file mode 100644 index 00000000000..6da40f3c443 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.RequestBuilder; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +import java.io.IOException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +final class ChunkedRequest { + + private ChunkedRequest() { + // hide + } + + public static ContentChannel executeRequest(AsyncHttpClient ningClient, Request request, HttpRequest.Method method, + ResponseHandler handler, Metric metric, Metric.Context ctx) + { + RequestBuilder builder = RequestBuilderFactory.newInstance(request, method); + ChunkedRequestContent content = new ChunkedRequestContent(request); + builder.setBody(content); + try { + ningClient.executeRequest(builder.build(), new AsyncResponseHandler(request, handler, metric, ctx)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return content; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java new file mode 100644 index 00000000000..9142910a91d --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.Body; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ChunkedRequestBody implements Body { + + private final ChunkedRequestContent content; + private ByteBuffer currentBuf; + + public ChunkedRequestBody(ChunkedRequestContent content) { + this.content = content; + } + + @Override + public long getContentLength() { + return -1; // unknown + } + + @Override + public long read(ByteBuffer dst) throws IOException { + if (content.isEndOfInput()) { + return -1; + } + if (currentBuf == null || currentBuf.remaining() == 0) { + currentBuf = content.nextChunk(); + } + if (currentBuf == null) { + return 0; + } + int len = Math.min(currentBuf.remaining(), dst.remaining()); + for (int i = 0; i < len; ++i) { + dst.put(currentBuf.get()); + } + return len; + } + + @Override + public void close() throws IOException { + + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java new file mode 100644 index 00000000000..265315d3eb8 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java @@ -0,0 +1,124 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.Body; +import com.ning.http.client.BodyGenerator; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.http.core.HeaderFieldsUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ChunkedRequestContent implements BodyGenerator, ContentChannel { + + private static final byte[] LAST_CHUNK = "0\r\n".getBytes(StandardCharsets.UTF_8); + private static final byte[] CRLF_BYTES = "\r\n".getBytes(StandardCharsets.UTF_8); + private final AtomicReference<ChunkedRequestBody> body = new AtomicReference<>(new ChunkedRequestBody(this)); + private final AtomicBoolean writerClosed = new AtomicBoolean(false); + private final Queue<Entry> writeQueue = new ConcurrentLinkedQueue<>(); + private final Queue<ByteBuffer> readQueue = new LinkedList<>(); + private final Request request; + private boolean readerClosed = false; + + public ChunkedRequestContent(Request request) { + this.request = request; + } + + @Override + public Body createBody() throws IOException { + // this is called by Netty, and presumably has to be thread-safe since Netty assigns thread by connection -- + // retries are necessarily done using new connections + Body body = this.body.getAndSet(null); + if (body == null) { + throw new UnsupportedOperationException("ChunkedRequestContent does not support retries."); + } + return body; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + // this can be called by any JDisc thread, and needs to be thread-safe + Objects.requireNonNull(buf, "buf"); + if (writerClosed.get()) { + throw new IllegalStateException("ChunkedRequestContent is closed."); + } + writeQueue.add(new Entry(buf, handler)); + } + + @Override + public void close(CompletionHandler handler) { + // this can be called by any JDisc thread, and needs to be thread-safe + if (writerClosed.getAndSet(true)) { + throw new IllegalStateException("ChunkedRequestContent already closed."); + } + writeQueue.add(new Entry(null, handler)); + } + + public ByteBuffer nextChunk() { + // this method is only called by the ChunkedRequestBody, which in turns is only called by the thread assigned to + // the underlying Netty connection -- it does not need to be thread-safe + if (!readQueue.isEmpty()) { + ByteBuffer buf = readQueue.poll(); + if (buf == null) { + readerClosed = true; + } + return buf; + } + if (writeQueue.isEmpty()) { + return null; + } + Entry entry = writeQueue.poll(); + try { + entry.handler.completed(); + } catch (Exception e) { + // TODO: fail and close write queue + // TODO: rethrow e to make ning abort request + } + if (entry.buf != null) { + readQueue.add(ByteBuffer.wrap(Integer.toHexString(entry.buf.remaining()).getBytes(StandardCharsets.UTF_8))); + readQueue.add(ByteBuffer.wrap(CRLF_BYTES)); + readQueue.add(entry.buf); + readQueue.add(ByteBuffer.wrap(CRLF_BYTES)); + } else { + readQueue.add(ByteBuffer.wrap(LAST_CHUNK)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HeaderFieldsUtil.copyTrailers(request, out); + byte[] buf = out.toByteArray(); + if (buf.length > 0) { + readQueue.add(ByteBuffer.wrap(buf)); + } + readQueue.add(ByteBuffer.wrap(CRLF_BYTES)); + readQueue.add(null); + } + return readQueue.poll(); + } + + public boolean isEndOfInput() { + // only called by the assigned Netty thread, does not need to be thread-safe + return readerClosed; + } + + private static class Entry { + + final ByteBuffer buf; + final CompletionHandler handler; + + Entry(ByteBuffer buf, CompletionHandler handler) { + this.buf = buf; + this.handler = handler; + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java new file mode 100644 index 00000000000..30a3809e30a --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +final class EmptyRequest { + + private EmptyRequest() { + // hide + } + + public static ContentChannel executeRequest(AsyncHttpClient ningClient, Request request, HttpRequest.Method method, + ResponseHandler handler, Metric metric, Metric.Context ctx) { + return new EmptyRequestContent(ningClient, request, method, + new AsyncResponseHandler(request, handler, metric, ctx)); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java new file mode 100644 index 00000000000..fc29bc9da6e --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.RequestBuilder; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.core.HeaderFieldsUtil; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class EmptyRequestContent implements ContentChannel { + + private final AtomicBoolean closed = new AtomicBoolean(false); + private final AsyncHttpClient client; + private final AsyncResponseHandler handler; + private final Request request; + private final HttpRequest.Method method; + + public EmptyRequestContent(AsyncHttpClient client, Request request, HttpRequest.Method method, + AsyncResponseHandler handler) { + this.client = client; + this.request = request; + this.method = method; + this.handler = handler; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + throw new UnsupportedOperationException("Request does not support a message-body."); + } + + @Override + public void close(CompletionHandler handler) { + if (closed.getAndSet(true)) { + if (handler != null) { + handler.completed(); + } + return; + } + try { + executeRequest(); + handler.completed(); + } catch (Exception e) { + try { + handler.failed(e); + } catch (Exception f) { + // ignore + } + } + } + + private void executeRequest() throws IOException { + RequestBuilder builder = RequestBuilderFactory.newInstance(request, method); + HeaderFieldsUtil.copyTrailers(request, builder); + client.executeRequest(builder.build(), handler); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java new file mode 100644 index 00000000000..19e9190a32b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java @@ -0,0 +1,121 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.Cookie; +import com.ning.http.client.FluentCaseInsensitiveStringsMap; +import com.ning.http.client.Response; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + * @since 2.0 + */ +final class EmptyResponse implements Response { + + public static final EmptyResponse INSTANCE = new EmptyResponse(); + + private EmptyResponse() { + // hide + } + + @Override + public int getStatusCode() { + return 0; + } + + @Override + public String getStatusText() { + return null; + } + + @Override + public ByteBuffer getResponseBodyAsByteBuffer() { + return ByteBuffer.allocate(0); + } + + @Override + public byte[] getResponseBodyAsBytes() throws IOException { + return new byte[0]; + } + + @Override + public InputStream getResponseBodyAsStream() throws IOException { + return null; + } + + @Override + public String getResponseBodyExcerpt(int maxLength, String charset) throws IOException { + return null; + } + + @Override + public String getResponseBody(String charset) throws IOException { + return null; + } + + @Override + public String getResponseBodyExcerpt(int maxLength) throws IOException { + return null; + } + + @Override + public String getResponseBody() throws IOException { + return null; + } + + @Override + public URI getUri() throws MalformedURLException { + return null; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public String getHeader(String name) { + return null; + } + + @Override + public List<String> getHeaders(String name) { + return null; + } + + @Override + public FluentCaseInsensitiveStringsMap getHeaders() { + return null; + } + + @Override + public boolean isRedirected() { + return false; + } + + @Override + public List<Cookie> getCookies() { + return null; + } + + @Override + public boolean hasResponseStatus() { + return false; + } + + @Override + public boolean hasResponseHeaders() { + return false; + } + + @Override + public boolean hasResponseBody() { + return false; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java new file mode 100644 index 00000000000..495fd303ad2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java @@ -0,0 +1,264 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.google.inject.Inject; +import com.ning.http.client.AsyncHandler; +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.filter.FilterContext; +import com.ning.http.client.filter.FilterException; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.jdisc.http.client.filter.ResponseFilter; +import com.yahoo.jdisc.http.client.filter.core.ResponseFilterBridge; +import com.yahoo.jdisc.http.ssl.JKSKeyStore; +import com.yahoo.jdisc.http.ssl.SslContextFactory; +import com.yahoo.jdisc.http.ssl.SslKeyStore; +import com.yahoo.jdisc.service.AbstractClientProvider; +import com.yahoo.vespa.defaults.Defaults; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import java.net.URI; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class HttpClient extends AbstractClientProvider { + + public interface Metrics { + + String NUM_REQUESTS = "clientRequests"; + String NUM_RESPONSES = "clientResponses"; + String REQUEST_LATENCY = "clientRequestLatency"; + String CONNECTION_EXCEPTIONS = "clientConnectExceptions"; + String TIMEOUT_EXCEPTIONS = "clientTimeoutExceptions"; + String OTHER_EXCEPTIONS = "clientOtherExceptions"; + String NUM_BYTES_RECEIVED = "ClientBytesReceived"; + String NUM_BYTES_SENT = "ClientBytesSent"; + String TOTAL_LATENCY = "ClientTotalResponseLatency"; + String TRANSFER_LATENCY = "ClientDataTransferLatency"; + } + + private static final String WEBSOCKET = "ws"; + private static final String HTTP = "http"; + private static final String HTTPS = "https"; + + private final ConcurrentMap<String, Metric.Context> metricCtx = new ConcurrentHashMap<>(); + private final Object metricCtxLock = new Object(); + private final AsyncHttpClient ningClient; + private final Metric metric; + private final boolean chunkedEncodingEnabled; + + protected HttpClient(HttpClientConfig config, ThreadFactory threadFactory, Metric metric, + HostnameVerifier hostnameVerifier, SSLContext sslContext, + List<ResponseFilter> responseFilters) { + this.ningClient = newNingClient(config, threadFactory, hostnameVerifier, sslContext, responseFilters); + this.metric = metric; + this.chunkedEncodingEnabled = config.chunkedEncodingEnabled(); + } + + /** Create a client which cannot look up secrets for use in requests */ + public HttpClient(HttpClientConfig config, ThreadFactory threadFactory, Metric metric, + HostnameVerifier hostnameVerifier, List<ResponseFilter> responseFilters) { + this(config, threadFactory, metric, hostnameVerifier, resolveSslContext(config.ssl(), new ThrowingSecretStore()), responseFilters); + } + + @Inject + public HttpClient(HttpClientConfig config, ThreadFactory threadFactory, Metric metric, + HostnameVerifier hostnameVerifier, List<ResponseFilter> responseFilters, SecretStore secretStore) { + this(config, threadFactory, metric, hostnameVerifier, resolveSslContext(config.ssl(), secretStore), responseFilters); + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + Metric.Context ctx = newMetricContext(request.getUri()); + String uriScheme = request.getUri().getScheme(); + + switch (uriScheme) { + case WEBSOCKET: + return WebSocketClientRequest.executeRequest(ningClient, request, handler, metric, ctx); + case HTTP: + case HTTPS: + HttpRequest.Method method = resolveMethod(request); + metric.add(Metrics.NUM_BYTES_SENT, request.headers().size(), ctx); + + if (!hasMessageBody(method)) { + return EmptyRequest.executeRequest(ningClient, request, method, handler, metric, ctx); + } + if (isChunkedEncodingEnabled(request, method)) { + return ChunkedRequest.executeRequest(ningClient, request, method, handler, metric, ctx); + } + return BufferedRequest.executeRequest(ningClient, request, method, handler, metric, ctx); + default: + throw new UnsupportedOperationException("Unknown protocol: " + uriScheme); + } + } + + @Override + protected void destroy() { + ningClient.close(); + } + + private HttpRequest.Method resolveMethod(Request request) { + if (request instanceof HttpRequest) { + return ((HttpRequest)request).getMethod(); + } + return HttpRequest.Method.POST; + } + + private boolean hasMessageBody(HttpRequest.Method method) { + return method != HttpRequest.Method.TRACE; + } + + private boolean isChunkedEncodingEnabled(Request request, HttpRequest.Method method) { + if (!chunkedEncodingEnabled) { + return false; + } + if (method == HttpRequest.Method.GET || method == HttpRequest.Method.HEAD) { + return false; + } + if (request.headers().isTrue(HttpHeaders.Names.X_DISABLE_CHUNKING)) { + return false; + } + if (request.headers().containsKey(HttpHeaders.Names.CONTENT_LENGTH)) { + return false; + } + if (request instanceof HttpRequest && ((HttpRequest)request).getVersion() == HttpRequest.Version.HTTP_1_0) { + return false; + } + return true; + } + + private Metric.Context newMetricContext(URI uri) { + String key = uri.getScheme() + "://" + uri.getHost() + (uri.getPort() != -1 ? ":" + uri.getPort() : ""); + Metric.Context ctx = metricCtx.get(key); + if (ctx == null) { + synchronized (metricCtxLock) { + ctx = metricCtx.get(key); + if (ctx == null) { + Map<String, Object> props = new HashMap<>(); + props.put("requestUri", key); + + ctx = metric.createContext(props); + if (ctx == null) { + ctx = NullContext.INSTANCE; + } + metricCtx.put(key, ctx); + } + } + } + if (ctx == NullContext.INSTANCE) { + return null; + } + return ctx; + } + + private static SSLContext resolveSslContext(HttpClientConfig.Ssl config, SecretStore secretStore) { + if (!config.enabled()) { + return null; + } + SslKeyStore keyStore = new JKSKeyStore(Paths.get(Defaults.getDefaults().underVespaHome(config.keyStorePath()))); + SslKeyStore trustStore = new JKSKeyStore(Paths.get(Defaults.getDefaults().underVespaHome(config.trustStorePath()))); + + String password = secretStore.getSecret(config.keyDBKey()); + keyStore.setKeyStorePassword(password); + trustStore.setKeyStorePassword(password); + SslContextFactory sslContextFactory = SslContextFactory.newInstance( + config.algorithm(), + config.protocol(), + keyStore, + trustStore); + return sslContextFactory.getServerSSLContext(); + } + + + @SuppressWarnings("deprecation") + private static AsyncHttpClient newNingClient(HttpClientConfig config, ThreadFactory threadFactory, + HostnameVerifier hostnameVerifier, SSLContext sslContext, + List<ResponseFilter> responseFilters) { + AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder(); + builder.setAllowPoolingConnection(config.connectionPoolEnabled()); + builder.setAllowSslConnectionPool(config.sslConnectionPoolEnabled()); + builder.setCompressionEnabled(config.compressionEnabled()); + builder.setConnectionTimeoutInMs((int)(config.connectionTimeout() * 1000)); + builder.setExecutorService(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, + threadFactory)); + builder.setFollowRedirects(config.followRedirects()); + builder.setHostnameVerifier(hostnameVerifier); + builder.setIOThreadMultiplier(2); + builder.setIdleConnectionInPoolTimeoutInMs((int)(config.idleConnectionInPoolTimeout() * 1000)); + builder.setIdleConnectionTimeoutInMs((int)(config.idleConnectionTimeout() * 1000)); + builder.setMaxRequestRetry(config.chunkedEncodingEnabled() ? 0 : config.maxNumRetries()); + builder.setMaximumConnectionsPerHost(config.maxNumConnectionsPerHost()); + builder.setMaximumConnectionsTotal(config.maxNumConnections()); + builder.setMaximumNumberOfRedirects(config.maxNumRedirects()); + if (!config.proxyServer().isEmpty()) { + builder.setProxyServer(ProxyServerFactory.newInstance(URI.create(config.proxyServer()))); + } + builder.setRemoveQueryParamsOnRedirect(config.removeQueryParamsOnRedirect()); + builder.setRequestCompressionLevel(config.compressionLevel()); + builder.setRequestTimeoutInMs((int)(config.requestTimeout() * 1000)); + builder.setSSLContext(sslContext); + builder.setUseProxyProperties(config.useProxyProperties()); + builder.setUseRawUrl(config.useRawUri()); + builder.setUserAgent(config.userAgent()); + builder.setWebSocketIdleTimeoutInMs((int)(config.idleWebSocketTimeout() * 1000)); + + for (final ResponseFilter responseFilter : responseFilters) { + builder.addResponseFilter(new com.ning.http.client.filter.ResponseFilter() { + @Override + @SuppressWarnings("rawtypes") + public FilterContext filter(FilterContext filterContext) throws FilterException { + /* + * TODO: returned ResponseFilterContext is ignored right now. + * For now, we return the input filterContext until there is a need for custom filterContext + * (which will complicate the code quite a bit since we are abstracting the Ning client) + */ + Request request = null; + AsyncHandler<?> handler = filterContext.getAsyncHandler(); + if (handler instanceof AsyncResponseHandler) { + request = ((AsyncResponseHandler)handler).getRequest(); + } + try { + // We do not retain the request here since this is executed before the response handler + responseFilter.filter(ResponseFilterBridge.toResponseFilterContext(filterContext, request)); + } catch (com.yahoo.jdisc.http.client.filter.FilterException e) { + throw new FilterException(e.getMessage()); + } + return filterContext; + } + } + ); + } + return new AsyncHttpClient(builder.build()); + } + + private static class NullContext implements Metric.Context { + + static final NullContext INSTANCE = new NullContext(); + } + + private static final class ThrowingSecretStore implements SecretStore { + + @Override + public String getSecret(String key) { + throw new UnsupportedOperationException("A secret store is not available"); + } + + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java new file mode 100644 index 00000000000..37c7ce2ac67 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.ProxyServer; + +import java.net.URI; +import java.util.Locale; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + * @since 2.0 + */ +final class ProxyServerFactory { + + private ProxyServerFactory() { + // hide + } + + public static ProxyServer newInstance(URI uri) { + if (uri == null) { + return null; + } + String userInfo = uri.getUserInfo(); + String username = null, password = null; + if (userInfo != null) { + String[] arr = userInfo.split(":", 2); + username = arr[0]; + password = arr.length > 1 ? arr[1] : null; + } + return new ProxyServer(ProxyServer.Protocol.valueOf(uri.getScheme().toUpperCase(Locale.US)), + uri.getHost(), uri.getPort(), username, password); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java new file mode 100644 index 00000000000..b304ba8a1b2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.RequestBuilder; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.core.HeaderFieldsUtil; + +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +final class RequestBuilderFactory { + + private RequestBuilderFactory() { + // hide + } + + public static RequestBuilder newInstance(Request request, HttpRequest.Method method) { + RequestBuilder builder = new RequestBuilder(); + if (request instanceof HttpRequest) { + HttpRequest httpRequest = (HttpRequest)request; + builder.setProxyServer(ProxyServerFactory.newInstance(httpRequest.getProxyServer())); + + Long timeout = httpRequest.getConnectionTimeout(TimeUnit.MILLISECONDS); + if (timeout != null) { + // TODO: Uncomment the next line once ticket 5536510 has been resolved. + // builder.setConnectTimeout(timeout); + } + } + builder.setMethod(method.name()); + builder.setUrl(request.getUri().toString()); + HeaderFieldsUtil.copyHeaders(request, builder); + return builder; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java new file mode 100644 index 00000000000..9df75e93b92 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.websocket.WebSocketUpgradeHandler; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +final class WebSocketClientRequest { + + private WebSocketClientRequest() { + // hide + } + + public static ContentChannel executeRequest(AsyncHttpClient client, Request request, + ResponseHandler responseHandler, Metric metric, Metric.Context ctx) { + return new WebSocketContent(client, request, new WebSocketUpgradeHandler.Builder() + .addWebSocketListener(new WebSocketHandler(request, responseHandler, metric, ctx)) + .build()); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java new file mode 100644 index 00000000000..4331620513d --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.RequestBuilder; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketUpgradeHandler; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; + +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * A content channel for interfacing with the web socket client. It accumulates the request data + * before dispatching it to the remote endpoint. + * + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +class WebSocketContent implements ContentChannel { + + private final AsyncHttpClient client; + private final Request request; + private final WebSocketUpgradeHandler handler; + private final Object wsLock = new Object(); + private WebSocket websocket; + + WebSocketContent(AsyncHttpClient client, Request request, WebSocketUpgradeHandler handler) { + this.client = client; + this.request = request; + this.handler = handler; + this.websocket = null; + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + Objects.requireNonNull(buf, "buf"); + + try { + executeRequest(buf.array()); + if (handler != null) { + handler.completed(); + } + } catch (Exception e) { + if (websocket != null) { + websocket.close(); + } + + throw new RuntimeException(e); + } + } + + @Override + public void close(CompletionHandler handler) { + if (websocket != null) { + websocket.close(); + } + + if (handler != null) { + handler.completed(); + } + } + + private void executeRequest(final byte[] content) throws Exception { + RequestBuilder builder = new RequestBuilder(); + builder.setUrl(request.getUri().toString()); + + synchronized (wsLock) { + if (websocket == null) { + websocket = client.executeRequest(builder.build(), handler).get(); + } + } + + if (websocket.isOpen()) { + websocket.sendMessage(content); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java new file mode 100644 index 00000000000..9b1540881eb --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java @@ -0,0 +1,159 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketByteListener; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpResponse; + +import java.net.ConnectException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeoutException; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +class WebSocketHandler implements WebSocketByteListener { + + private final CompletionHandler abortOnFailure = new AbortOnFailure(); + private final Metric metric; + private final Metric.Context metricCtx; + private final Request request; + private final ResponseHandler responseHandler; + private ContentChannel content; + private boolean aborted = false; + + public WebSocketHandler(Request request, ResponseHandler responseHandler, Metric metric, Metric.Context ctx) { + this.request = request; + this.responseHandler = responseHandler; + this.metric = metric; + this.metricCtx = ctx; + } + + @Override + public synchronized void onOpen(WebSocket webSocket) { + // ignore, open on first fragment to allow failures to propagate + } + + @Override + public synchronized void onMessage(byte[] bytes) { + if (aborted) { + return; + } + if (content == null) { + dispatchResponse(); + } + // need to copy the bytes into a new buffer since there is no declared ownership of the array + content.write((ByteBuffer)ByteBuffer.allocate(bytes.length).put(bytes).flip(), abortOnFailure); + } + + @Override + public synchronized void onFragment(byte[] bytes, boolean last) { + // ignore, write messages instead + } + + @Override + public synchronized void onClose(WebSocket webSocket) { + if (aborted) { + return; + } + if (content == null) { + dispatchResponse(); + } + content.close(abortOnFailure); + } + + @Override + public synchronized void onError(Throwable t) { + abort(t); + } + + private void dispatchResponse() { + content = responseHandler.handleResponse(HttpResponse.newInstance(Response.Status.OK)); + } + + private synchronized void abort(Throwable t) { + if (aborted) { + return; + } + aborted = true; + updateErrorMetric(t); + if (content == null) { + dispatchErrorResponse(t); + } + if (content != null) { + terminateContent(); + } + } + + private void updateErrorMetric(Throwable t) { + try { + if (t instanceof ConnectException) { + metric.add(HttpClient.Metrics.CONNECTION_EXCEPTIONS, 1, metricCtx); + } else if (t instanceof TimeoutException) { + metric.add(HttpClient.Metrics.TIMEOUT_EXCEPTIONS, 1, metricCtx); + } else { + metric.add(HttpClient.Metrics.OTHER_EXCEPTIONS, 1, metricCtx); + } + } catch (Exception e) { + // ignore + } + } + + private void dispatchErrorResponse(Throwable t) { + int status; + if (t instanceof ConnectException) { + status = com.yahoo.jdisc.Response.Status.SERVICE_UNAVAILABLE; + } else if (t instanceof TimeoutException) { + status = com.yahoo.jdisc.Response.Status.REQUEST_TIMEOUT; + } else { + status = com.yahoo.jdisc.Response.Status.BAD_REQUEST; + } + try { + content = responseHandler.handleResponse(HttpResponse.newError(request, status, t)); + } catch (Exception e) { + // ignore + } + } + + private void terminateContent() { + try { + content.close(IgnoreFailure.INSTANCE); + } catch (Exception e) { + // ignore + } + } + + private class AbortOnFailure implements CompletionHandler { + + @Override + public void completed() { + + } + + @Override + public void failed(Throwable t) { + abort(t); + } + } + + private static class IgnoreFailure implements CompletionHandler { + + final static IgnoreFailure INSTANCE = new IgnoreFailure(); + + @Override + public void completed() { + + } + + @Override + public void failed(Throwable t) { + + } + } +}
\ No newline at end of file diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java new file mode 100644 index 00000000000..b9cb5c3fac3 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java @@ -0,0 +1,12 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client.filter; + +/** + * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a> + */ +public class FilterException extends Exception { + + public FilterException(String msg) { + super(msg); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java new file mode 100644 index 00000000000..5fffc8312d7 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client.filter; + +/** + * This interface can be implemented to define custom behavior that gets invoked before the response bytes are processed. + * Authorization, proxy authentication and redirects processing all happen after the filters get executed. + * + * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a> + */ +public interface ResponseFilter { + + public ResponseFilterContext filter(ResponseFilterContext filterContext) throws FilterException; + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java new file mode 100644 index 00000000000..4f956220398 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client.filter; + +import com.google.common.collect.ImmutableMap; +import com.ning.http.client.FluentCaseInsensitiveStringsMap; + +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a> + */ +public class ResponseFilterContext { + + private final FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap(); + private final Map<String, Object> requestContext; + private int statusCode; + private URI uri; + + private ResponseFilterContext(Builder builder) { + this.statusCode = builder.statusCode; + this.uri = builder.uri; + this.headers.putAll(builder.headers); + requestContext = ImmutableMap.copyOf(builder.requestContext); + } + + public URI getRequestURI() { + return uri; + } + + public Map<String, Object> getRequestContext() { return requestContext; } + + public String getResponseFirstHeader(String key) { + return headers.getFirstValue(key); + } + + public int getResponseStatusCode() { + return statusCode; + } + + public static class Builder { + + private final FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap(); + private final Map<String, Object> requestContext = new HashMap<>(); + private int statusCode; + private URI uri; + + public Builder() { + } + + public Builder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public Builder headers(FluentCaseInsensitiveStringsMap headers) { + this.headers.putAll(headers); + return this; + } + + public Builder uri(URI uri) { + this.uri = uri; + return this; + } + + public Builder requestContext(Map<String, Object> requestContext) { + this.requestContext.putAll(requestContext); + return this; + } + + public ResponseFilterContext build() { + return new ResponseFilterContext(this); + } + + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java new file mode 100644 index 00000000000..6d895ad0f93 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client.filter.core; + +import com.ning.http.client.filter.FilterContext; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.http.client.filter.ResponseFilterContext; + +import java.util.Collections; + +/** + * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a> + */ +public class ResponseFilterBridge { + + public static ResponseFilterContext toResponseFilterContext(FilterContext<?> filterContext, Request request) { + return new ResponseFilterContext.Builder() + .uri(filterContext.getRequest().getURI()) + .statusCode(filterContext.getResponseStatus().getStatusCode()) + .headers(filterContext.getResponseHeaders().getHeaders()) + .requestContext(request == null ? Collections.<String, Object>emptyMap() : request.context()) + .build(); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java new file mode 100644 index 00000000000..4ce70c28623 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java @@ -0,0 +1,4 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.client.filter; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java new file mode 100644 index 00000000000..5d5ec2c1ab8 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java @@ -0,0 +1,4 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.client; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java new file mode 100644 index 00000000000..7132cee91c0 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java @@ -0,0 +1,4 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.cloud; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java new file mode 100644 index 00000000000..7f85c8f9c2d --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.core; + +import com.yahoo.jdisc.handler.CompletionHandler; + +import java.util.Arrays; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class CompletionHandlers { + + public static void tryComplete(CompletionHandler handler) { + if (handler == null) { + return; + } + try { + handler.completed(); + } catch (Exception e) { + // ignore + } + } + + public static void tryFail(CompletionHandler handler, Throwable t) { + if (handler == null) { + return; + } + try { + handler.failed(t); + } catch (Exception e) { + // ignore + } + } + + public static CompletionHandler wrap(CompletionHandler... handlers) { + return wrap(Arrays.asList(handlers)); + } + + public static CompletionHandler wrap(final Iterable<CompletionHandler> handlers) { + return new CompletionHandler() { + + @Override + public void completed() { + for (CompletionHandler handler : handlers) { + tryComplete(handler); + } + } + + @Override + public void failed(Throwable t) { + for (CompletionHandler handler : handlers) { + tryFail(handler, t); + } + } + }; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java new file mode 100644 index 00000000000..065276962f7 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java @@ -0,0 +1,142 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.core; + +import com.ning.http.client.RequestBuilder; +import com.yahoo.jdisc.HeaderFields; +import org.jboss.netty.handler.codec.http.HttpChunkTrailer; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpMessage; +import org.jboss.netty.handler.codec.http.HttpResponse; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class HeaderFieldsUtil { + + private static final byte[] DELIM_BYTES = ": ".getBytes(StandardCharsets.UTF_8); + private static final byte[] CRLF_BYTES = "\r\n".getBytes(StandardCharsets.UTF_8); + private static final Set<String> IGNORED_HEADERS = new HashSet<>(Arrays.asList( + HttpHeaders.Names.CONTENT_LENGTH, + HttpHeaders.Names.TRANSFER_ENCODING)); + + public static void copyHeaders(com.yahoo.jdisc.Response src, HttpResponse dst) { + copyHeaderFields(src.headers(), newSimpleHeaders(dst)); + } + + public static void copyHeaders(com.yahoo.jdisc.Request src, RequestBuilder dst) { + copyHeaderFields(src.headers(), newSimpleHeaders(dst)); + } + + public static void copyTrailers(com.yahoo.jdisc.Response src, HttpResponse dst) { + copyTrailers(src, newSimpleHeaders(dst)); + } + + public static void copyTrailers(com.yahoo.jdisc.Response src, HttpChunkTrailer dst) { + copyTrailers(src, newSimpleHeaders(dst)); + } + + public static void copyTrailers(com.yahoo.jdisc.Request src, RequestBuilder dst) { + copyTrailers(src, newSimpleHeaders(dst)); + } + + public static void copyTrailers(com.yahoo.jdisc.Request src, ByteArrayOutputStream dst) { + copyTrailers(src, newSimpleHeaders(dst)); + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + public static void copyTrailers(com.yahoo.jdisc.Request src, SimpleHeaders dst) { + if (!(src instanceof com.yahoo.jdisc.http.HttpRequest)) { + return; + } + final HeaderFields trailers = ((com.yahoo.jdisc.http.HttpRequest)src).trailers(); + synchronized (trailers) { + copyHeaderFields(trailers, dst); + } + } + + @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") + public static void copyTrailers(com.yahoo.jdisc.Response src, SimpleHeaders dst) { + if (!(src instanceof com.yahoo.jdisc.http.HttpResponse)) { + return; + } + final HeaderFields trailers = ((com.yahoo.jdisc.http.HttpResponse)src).trailers(); + synchronized (trailers) { + copyHeaderFields(trailers, dst); + } + } + + private static void copyHeaderFields(HeaderFields src, SimpleHeaders dst) { + for (Map.Entry<String, List<String>> entry : src.entrySet()) { + String key = entry.getKey(); + if (key != null && !IGNORED_HEADERS.contains(key)) { + if (entry.getValue() == null) { + dst.addHeader(key, ""); + continue; + } + for (String value : entry.getValue()) { + dst.addHeader(key, value != null ? value : ""); + } + } + } + } + + private static SimpleHeaders newSimpleHeaders(final RequestBuilder dst) { + return new SimpleHeaders() { + + @Override + public void addHeader(String name, String value) { + dst.addHeader(name, value); + } + }; + } + + private static SimpleHeaders newSimpleHeaders(final ByteArrayOutputStream dst) { + return new SimpleHeaders() { + + @Override + public void addHeader(String name, String value) { + safeWrite(name.getBytes(StandardCharsets.UTF_8)); + safeWrite(DELIM_BYTES); + safeWrite(value.getBytes(StandardCharsets.UTF_8)); + safeWrite(CRLF_BYTES); + } + + void safeWrite(byte[] buf) { + dst.write(buf, 0, buf.length); + } + }; + } + + private static SimpleHeaders newSimpleHeaders(final HttpMessage dst) { + return new SimpleHeaders() { + + @Override + public void addHeader(String name, String value) { + dst.addHeader(name, value); + } + }; + } + + private static SimpleHeaders newSimpleHeaders(final HttpChunkTrailer dst) { + return new SimpleHeaders() { + + @Override + public void addHeader(String name, String value) { + dst.addHeader(name, value); + } + }; + } + + private static interface SimpleHeaders { + + public void addHeader(String name, String value); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java new file mode 100644 index 00000000000..649bd2cf517 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java @@ -0,0 +1,544 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.Principal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Pattern; + + +import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest; +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Version; + +/** + * The Request class on which all filters will operate upon. + * <p> + * This class was made abstract from 5.27. Test cases that need a concrete + * instance should create a {@link JdiscFilterRequest}. + */ + +public abstract class DiscFilterRequest { + + protected static final String HTTPS_PREFIX = "https"; + protected static final int DEFAULT_HTTP_PORT = 80; + protected static final int DEFAULT_HTTPS_PORT = 443; + + private final ServletOrJdiscHttpRequest parent; + protected final InetSocketAddress localAddress; + protected final Map<String, List<String>> untreatedParams; + private final HeaderFields untreatedHeaders; + private List<Cookie> untreatedCookies = null; + private Principal userPrincipal = null; + private String remoteUser = null; + private String[] roles = null; + private boolean overrideIsUserInRole = false; + + public DiscFilterRequest(ServletOrJdiscHttpRequest parent) { + this.parent = parent; + + //save untreated headers from parent + untreatedHeaders = new HeaderFields(); + parent.copyHeaders(untreatedHeaders); + + untreatedParams = new HashMap<>(parent.parameters()); + + int port = parent.getUri().getPort(); + if(port < 0) { + port = 0; + } + localAddress = new InetSocketAddress(parent.getUri().getHost(), port); + } + + public abstract String getMethod(); + + public Version getVersion() { + return parent.getVersion(); + } + + public URI getUri() { + return parent.getUri(); + } + + public abstract void setUri(URI uri); + + public HttpRequest getParentRequest() { + throw new UnsupportedOperationException( + "getParentRequest is not supported for " + parent.getClass().getName()); + } + + /** + * Returns the Internet Protocol (IP) address of the client + * or last proxy that sent the request. + */ + public String getRemoteAddr() { + return parent.getRemoteHostAddress(); + } + + /** + * Set the IP address of the remote client associated with this Request. + */ + public void setRemoteAddr(String remoteIpAddress) { + InetSocketAddress remoteAddress = new InetSocketAddress(remoteIpAddress, this.getRemotePort()); + parent.setRemoteAddress(remoteAddress); + } + + /** + * Returns the Internet Protocol (IP) address of the interface + * on which the request was received. + */ + public String getLocalAddr() { + if (null == localAddress.getAddress()) { + return null; + } + return localAddress.getAddress().getHostAddress(); + } + + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(parent.context().keySet()); + } + + public Object getAttribute(String name) { + return parent.context().get(name); + } + + public void setAttribute(String name, Object value) { + parent.context().put(name, value); + } + + public boolean containsAttribute(String name) { + return parent.context().containsKey(name); + } + + public void removeAttribute(String name) { + parent.context().remove(name); + } + + public abstract String getParameter(String name); + + public abstract Enumeration<String> getParameterNames(); + + public List<String> getParameterNamesAsList() { + return new ArrayList<String>(parent.parameters().keySet()); + } + + public Enumeration<String> getParameterValues(String name) { + return Collections.enumeration(parent.parameters().get(name)); + } + + public List<String> getParameterValuesAsList(String name) { + return parent.parameters().get(name); + } + + public Map<String,List<String>> getParameterMap() { + return parent.parameters(); + } + + + /** + * Returns the hostName of remoteHost, or null if none + */ + public String getRemoteHost() { + return parent.getRemoteHostName(); + } + + /** + * Returns the Internet Protocol (IP) port number of + * the interface on which the request was received. + */ + public int getLocalPort() { + return localAddress.getPort(); + } + + /** + * Returns the port of remote host + */ + public int getRemotePort() { + return parent.getRemotePort(); + } + + /** + * Returns a unmodifiable map of untreatedParameters from the + * parent request. + */ + public Map<String, List<String>> getUntreatedParams() { + return Collections.unmodifiableMap(untreatedParams); + } + + + /** + * Returns the untreatedHeaders from + * parent request + */ + public HeaderFields getUntreatedHeaders() { + return untreatedHeaders; + } + + /** + * Returns the untreatedCookies from + * parent request + */ + public List<Cookie> getUntreatedCookies() { + if(untreatedCookies == null) { + this.untreatedCookies = parent.decodeCookieHeader(); + } + return Collections.unmodifiableList(untreatedCookies); + } + + /** + * Sets a header with the given name and value. + * If the header had already been set, the new value overwrites the previous one. + */ + public abstract void addHeader(String name, String value); + + public long getDateHeader(String name) { + String value = getHeader(name); + if (value == null) + return -1L; + + Date date = null; + for (int i = 0; (date == null) && (i < formats.length); i++) { + try { + date = formats[i].parse(value); + } catch (ParseException e) { + } + } + if (date == null) { + return -1L; + } + + return date.getTime(); + } + + public abstract String getHeader(String name); + + public abstract Enumeration<String> getHeaderNames(); + + public abstract List<String> getHeaderNamesAsList(); + + public abstract Enumeration<String> getHeaders(String name); + + public abstract List<String> getHeadersAsList(String name); + + public abstract void removeHeaders(String name); + + /** + * Sets a header with the given name and value. + * If the header had already been set, the new value overwrites the previous one. + * + */ + public abstract void setHeaders(String name, String value); + + /** + * Sets a header with the given name and value. + * If the header had already been set, the new value overwrites the previous one. + * + */ + public abstract void setHeaders(String name, List<String> values); + + public int getIntHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } else { + return Integer.parseInt(value); + } + } + + + public List<Cookie> getCookies() { + return parent.decodeCookieHeader(); + } + + public void setCookies(List<Cookie> cookies) { + parent.encodeCookieHeader(cookies); + } + + public String getProtocol() { + return getVersion().name(); + } + + /** + * Returns the query string that is contained in the request URL. + * Returns the undecoded value uri.getRawQuery() + */ + public String getQueryString() { + return getUri().getRawQuery(); + } + + /** + * Returns the login of the user making this request, + * if the user has been authenticated, or null if the user has not been authenticated. + */ + public String getRemoteUser() { + return remoteUser; + } + + public String getRequestURI() { + return getUri().getRawPath(); + } + + public String getRequestedSessionId() { + return null; + } + + public String getScheme() { + return getUri().getScheme(); + } + + public void setScheme(String scheme, boolean isSecure) { + String uri = getUri().toString(); + String arr [] = uri.split("://"); + URI newUri = URI.create(scheme + "://" + arr[1]); + setUri(newUri); + } + + public String getServerName() { + return getUri().getHost(); + } + + public int getServerPort() { + int port = getUri().getPort(); + if(port == -1) { + if(isSecure()) { + port = DEFAULT_HTTPS_PORT; + } + else { + port = DEFAULT_HTTP_PORT; + } + } + + return port; + } + + public Principal getUserPrincipal() { + return userPrincipal; + } + + public boolean isSecure() { + if(getScheme().equalsIgnoreCase(HTTPS_PREFIX)) { + return true; + } + return false; + } + + + /** + * Returns a boolean indicating whether the authenticated user + * is included in the specified logical "role". + */ + public boolean isUserInRole(String role) { + if(overrideIsUserInRole) { + if(roles != null) { + for (String role1 : roles) { + if (role1 != null && role1.trim().length() > 0) { + String userRole = role1.trim(); + if (userRole.equals(role)) { + return true; + } + } + } + } + return false; + } + else { + return false; + } + } + + public void setOverrideIsUserInRole(boolean overrideIsUserInRole) { + this.overrideIsUserInRole = overrideIsUserInRole; + } + + public void setRemoteHost(String remoteAddr) { } + + public void setRemoteUser(String remoteUser) { + this.remoteUser = remoteUser; + } + + public void setUserPrincipal(Principal principal) { + this.userPrincipal = principal; + } + + public void setUserRoles(String[] roles) { + this.roles = roles; + } + + /** + * Returns the content-type for the request + */ + public String getContentType() { + return getHeader(HttpHeaders.Names.CONTENT_TYPE); + } + + + /** + * Get character encoding + */ + public String getCharacterEncoding() { + return getCharsetFromContentType(this.getContentType()); + } + + /** + * Set character encoding + */ + public void setCharacterEncoding(String encoding) { + String charEncoding = setCharsetFromContentType(this.getContentType(), encoding); + if(charEncoding != null && !charEncoding.isEmpty()) { + removeHeaders(HttpHeaders.Names.CONTENT_TYPE); + setHeaders(HttpHeaders.Names.CONTENT_TYPE, charEncoding); + } + } + + /** + * Can be called multiple times to add Cookies + */ + public void addCookie(JDiscCookieWrapper cookie) { + if(cookie != null) { + List<Cookie> cookies = new ArrayList<Cookie>(); + //Get current set of cookies first + List<Cookie> c = getCookies(); + if(c != null && !c.isEmpty()) { + cookies.addAll(c); + } + cookies.add(cookie.getCookie()); + setCookies(cookies); + } + } + + public abstract void clearCookies(); + + public JDiscCookieWrapper[] getWrappedCookies() { + List<Cookie> cookies = getCookies(); + if(cookies == null) { + return null; + } + List<JDiscCookieWrapper> cookieWrapper = new ArrayList<>(cookies.size()); + for(Cookie cookie : cookies) { + cookieWrapper.add(JDiscCookieWrapper.wrap(cookie)); + } + + return cookieWrapper.toArray(new JDiscCookieWrapper[cookieWrapper.size()]); + } + + private String setCharsetFromContentType(String contentType,String charset) { + String newContentType = ""; + if (contentType == null) + return (null); + int start = contentType.indexOf("charset="); + if (start < 0) { + //No charset present: + newContentType = contentType + ";charset=" + charset; + return newContentType; + } + String encoding = contentType.substring(start + 8); + int end = encoding.indexOf(';'); + if (end >= 0) { + newContentType = contentType.substring(0,start); + newContentType = newContentType + "charset=" + charset; + newContentType = newContentType + encoding.substring(end,encoding.length()); + } + else { + newContentType = contentType.substring(0,start); + newContentType = newContentType + "charset=" + charset; + } + + return (newContentType.trim()); + + } + + private String getCharsetFromContentType(String contentType) { + + if (contentType == null) + return (null); + int start = contentType.indexOf("charset="); + if (start < 0) + return (null); + String encoding = contentType.substring(start + 8); + int end = encoding.indexOf(';'); + if (end >= 0) + encoding = encoding.substring(0, end); + encoding = encoding.trim(); + if ((encoding.length() > 2) && (encoding.startsWith("\"")) + && (encoding.endsWith("\""))) + encoding = encoding.substring(1, encoding.length() - 1); + return (encoding.trim()); + + } + + public static boolean isMultipart(DiscFilterRequest request) { + if (request == null) { + return false; + } + + String contentType = request.getContentType(); + + if (contentType == null) { + return false; + } + + String[] parts = Pattern.compile(";").split(contentType); + if (parts.length == 0) { + return false; + } + + for (String part : parts) { + if ("multipart/form-data".equals(part)) { + return true; + } + } + + return false; + } + + protected static ThreadLocalSimpleDateFormat formats[] = { + new ThreadLocalSimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", + Locale.US), + new ThreadLocalSimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", + Locale.US), + new ThreadLocalSimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", + Locale.US) }; + + /** + * The set of SimpleDateFormat formats to use in getDateHeader(). + * + * Notice that because SimpleDateFormat is not thread-safe, we can't declare + * formats[] as a static variable. + */ + protected static final class ThreadLocalSimpleDateFormat extends + ThreadLocal<SimpleDateFormat> { + private final String format; + private final Locale locale; + + public ThreadLocalSimpleDateFormat(String format, Locale locale) { + super(); + this.format = format; + this.locale = locale; + } + + // @see java.lang.ThreadLocal#initialValue() + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat(format, locale); + } + + public Date parse(String value) throws ParseException { + return get().parse(value); + } + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java new file mode 100644 index 00000000000..84baf5c1177 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java @@ -0,0 +1,154 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; + + +import com.yahoo.jdisc.http.HttpResponse; + +/** + * This class was made abstract from 5.27. Test cases that need + * a concrete instance should create a {@link JdiscFilterResponse}. + * + * @author tejalk + */ +public abstract class DiscFilterResponse { + + private final ServletOrJdiscHttpResponse parent; + private final HeaderFields untreatedHeaders; + private final List<Cookie> untreatedCookies; + + public DiscFilterResponse(ServletOrJdiscHttpResponse parent) { + this.parent = parent; + + this.untreatedHeaders = new HeaderFields(); + parent.copyHeaders(untreatedHeaders); + + this.untreatedCookies = getCookies(); + } + + /* Attributes on the response are only used for unit testing. + * There is no such thing as 'attributes' in the underlying response. */ + + public Enumeration<String> getAttributeNames() { + return Collections.enumeration(parent.context().keySet()); + } + + public Object getAttribute(String name) { + return parent.context().get(name); + } + + public void setAttribute(String name, Object value) { + parent.context().put(name, value); + } + + public void removeAttribute(String name) { + parent.context().remove(name); + } + + /** + * Returns the untreatedHeaders from the parent request + */ + public HeaderFields getUntreatedHeaders() { + return untreatedHeaders; + } + + /** + * Returns the untreatedCookies from the parent request + */ + public List<Cookie> getUntreatedCookies() { + return untreatedCookies; + } + + /** + * Sets a header with the given name and value. + * <p> + * If the header had already been set, the new value overwrites the previous one. + */ + public abstract void setHeader(String name, String value); + + public abstract void removeHeaders(String name); + + /** + * Sets a header with the given name and value. + * <p> + * If the header had already been set, the new value overwrites the previous one. + */ + public abstract void setHeaders(String name, String value); + + /** + * Sets a header with the given name and value. + * <p> + * If the header had already been set, the new value overwrites the previous one. + */ + public abstract void setHeaders(String name, List<String> values); + + /** + * Adds a header with the given name and value + * @see com.yahoo.jdisc.HeaderFields#add + */ + public abstract void addHeader(String name, String value); + + public abstract String getHeader(String name); + + public List<Cookie> getCookies() { + return parent.decodeSetCookieHeader(); + } + + public abstract void setCookies(List<Cookie> cookies); + + public int getStatus() { + return parent.getStatus(); + } + + public abstract void setStatus(int status); + + /** + * Return the parent HttpResponse + */ + public HttpResponse getParentResponse() { + if (parent instanceof HttpResponse) + return (HttpResponse)parent; + throw new UnsupportedOperationException( + "getParentResponse is not supported for " + parent.getClass().getName()); + } + + public void addCookie(JDiscCookieWrapper cookie) { + if(cookie != null) { + List<Cookie> cookies = new ArrayList<>(); + //Get current set of cookies first + List<Cookie> c = getCookies(); + if((c != null) && (! c.isEmpty())) { + cookies.addAll(c); + } + cookies.add(cookie.getCookie()); + setCookies(cookies); + } + } + + /** + * This method does not actually send the response as it + * does not have access to responseHandler but + * just sets the status. The methodName is misleading + * for historical reasons. + */ + public void sendError(int errorCode) throws IOException { + setStatus(errorCode); + } + + public void setCookie(String name, String value) { + Cookie cookie = new Cookie(name, value); + setCookies(Arrays.asList(cookie)); + } + + } diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java new file mode 100644 index 00000000000..b01253536b6 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import java.util.Collection; + +/** + * Legacy filter config. Prefer to use a regular stringly typed config class for new filters. + * + * @author tejalk + */ +public interface FilterConfig { + + /** Returns the filter-name of this filter */ + String getFilterName(); + + /** Returns the filter-class of this filter */ + String getFilterClass(); + + /** + * Returns a String containing the value of the + * named initialization parameter, or null if + * the parameter does not exist. + * + * @param name a String specifying the name of the initialization parameter + * @return a String containing the value of the initialization parameter + */ + String getInitParameter(String name); + + /** + * Returns the boolean value of the init parameter. If not present returns default value + * + * @return boolean value of init parameter + */ + boolean getBooleanInitParameter(String name, boolean defaultValue); + + /** + * Returns the names of the filter's initialization parameters as an Collection of String objects, + * or an empty Collection if the filter has no initialization parameters. + */ + Collection<String> getInitParameterNames(); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java new file mode 100644 index 00000000000..c9765b648d2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import java.util.concurrent.TimeUnit; + +import com.yahoo.jdisc.http.Cookie; + +/** + * Wrapper of Cookie. + * + * @author tejalk + * + */ +public class JDiscCookieWrapper { + + private Cookie cookie; + + protected JDiscCookieWrapper(Cookie cookie) { + this.cookie = cookie; + } + + public static JDiscCookieWrapper wrap(Cookie cookie) { + return new JDiscCookieWrapper(cookie); + } + + public String getComment() { + return cookie.getComment(); + } + + public String getDomain() { + return cookie.getDomain(); + } + + public int getMaxAge() { + return cookie.getMaxAge(TimeUnit.SECONDS); + } + + public String getName() { + return cookie.getName(); + } + + public String getPath() { + return cookie.getPath(); + } + + public boolean getSecure() { + return cookie.isSecure(); + } + + public String getValue() { + return cookie.getValue(); + } + + public int getVersion() { + return cookie.getVersion(); + } + + public void setComment(String purpose) { + cookie.setComment(purpose); + } + + public void setDomain(String pattern) { + cookie.setDomain(pattern); + } + + public void setMaxAge(int expiry) { + cookie.setMaxAge(expiry, TimeUnit.SECONDS); + } + + public void setPath(String uri) { + cookie.setPath(uri); + } + + public void setSecure(boolean flag) { + cookie.setSecure(flag); + } + + public void setValue(String newValue) { + cookie.setValue(newValue); + } + + public void setVersion(int version) { + cookie.setVersion(version); + } + + /** + * Return com.yahoo.jdisc.http.Cookie + * + * @return - cookie + */ + public Cookie getCookie() { + return cookie; + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java new file mode 100644 index 00000000000..69de16a50c9 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java @@ -0,0 +1,110 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * JDisc implementation of a filter request. + * + * @since 5.27 + */ +public class JdiscFilterRequest extends DiscFilterRequest { + + private final HttpRequest parent; + + public JdiscFilterRequest(HttpRequest parent) { + super(parent); + this.parent = parent; + } + + public HttpRequest getParentRequest() { + return parent; + } + + public void setUri(URI uri) { + parent.setUri(uri); + } + + @Override + public String getMethod() { + return parent.getMethod().name(); + } + + @Override + public String getParameter(String name) { + if(parent.parameters().containsKey(name)) { + return parent.parameters().get(name).get(0); + } + else { + return null; + } + } + + @Override + public Enumeration<String> getParameterNames() { + return Collections.enumeration(parent.parameters().keySet()); + } + + @Override + public void addHeader(String name, String value) { + parent.headers().add(name, value); + } + + @Override + public String getHeader(String name) { + List<String> values = parent.headers().get(name); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(values.size() - 1); + } + + public Enumeration<String> getHeaderNames() { + return Collections.enumeration(parent.headers().keySet()); + } + + public List<String> getHeaderNamesAsList() { + return new ArrayList<String>(parent.headers().keySet()); + } + + @Override + public Enumeration<String> getHeaders(String name) { + return Collections.enumeration(getHeadersAsList(name)); + } + + public List<String> getHeadersAsList(String name) { + List<String> values = parent.headers().get(name); + if(values == null) { + return Collections.<String>emptyList(); + } + return parent.headers().get(name); + } + + @Override + public void removeHeaders(String name) { + parent.headers().remove(name); + } + + @Override + public void setHeaders(String name, String value) { + parent.headers().put(name, value); + } + + @Override + public void setHeaders(String name, List<String> values) { + parent.headers().put(name, values); + } + + @Override + public void clearCookies() { + parent.headers().remove(HttpHeaders.Names.COOKIE); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java new file mode 100644 index 00000000000..6d2a87cfa53 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpResponse; + +import java.util.List; + +/** + * JDisc implementation of a filter request. + * + * @since 5.27 + */ +public class JdiscFilterResponse extends DiscFilterResponse { + + private final HttpResponse parent; + + public JdiscFilterResponse(HttpResponse parent) { + super(parent); + this.parent = parent; + } + + @Override + public void setStatus(int status) { + parent.setStatus(status); + } + + @Override + public void setHeader(String name, String value) { + parent.headers().put(name, value); + } + + @Override + public void removeHeaders(String name) { + parent.headers().remove(name); + } + + @Override + public void setHeaders(String name, String value) { + parent.headers().put(name, value); + } + + @Override + public void setHeaders(String name, List<String> values) { + parent.headers().put(name, values); + } + + @Override + public void addHeader(String name, String value) { + parent.headers().add(name, value); + } + + @Override + public String getHeader(String name) { + List<String> values = parent.headers().get(name); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(values.size() - 1); + } + + @Override + public void setCookies(List<Cookie> cookies) { + parent.encodeSetCookieHeader(cookies); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java new file mode 100644 index 00000000000..8202ef0e693 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public interface RequestFilter extends com.yahoo.jdisc.SharedResource, RequestFilterBase { + + public void filter(HttpRequest request, ResponseHandler handler); +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java new file mode 100644 index 00000000000..47a41dfd6bc --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java @@ -0,0 +1,9 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +/** + * @author gjoranv + * @since 2.4 + */ +public interface RequestFilterBase { +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java new file mode 100644 index 00000000000..f03a16f0bf0 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.HttpRequest.Method; + +import java.net.URI; +import java.util.Optional; + +/** + * Read-only view of the request for use by SecurityResponseFilters. + * + * @author tonytv + */ +public interface RequestView { + + /** + * Returns a named attribute. + * + * @see <a href="http://docs.oracle.com/javaee/7/api/javax/servlet/ServletRequest.html#getAttribute%28java.lang.String%29">javax.servlet.ServletRequest.getAttribute(java.lang.String)</a> + * @see com.yahoo.jdisc.Request#context() + * @return the named data associated with the request that are private to this runtime (not exposed to the client) + */ + public Object getAttribute(String name); + + /** + * Returns the Http method. Only present if the underlying request has http-like semantics. + */ + public Optional<Method> getMethod(); + + public URI getUri(); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java new file mode 100644 index 00000000000..244ae056c33 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.SharedResource; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public interface ResponseFilter extends SharedResource, ResponseFilterBase { + + public void filter(Response response, Request request); +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java new file mode 100644 index 00000000000..c9bd1c8de67 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java @@ -0,0 +1,9 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +/** + * @author gjoranv + * @since 2.4 + */ +public interface ResponseFilterBase { +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java new file mode 100644 index 00000000000..52e05484afc --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.google.common.annotations.Beta; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.jdisc.http.servlet.ServletRequest; + +import com.yahoo.jdisc.http.servlet.ServletResponse; +import com.yahoo.jdisc.http.server.jetty.FilterInvoker; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.util.Optional; + +/** + * Only intended for internal vespa use. + * + * Runs JDisc security filter without using JDisc request/response. + * Only intended to be used in a servlet context, as the error messages are tailored for that. + * + * Assumes that SecurityResponseFilters mutate DiscFilterResponse in the thread they are invoked from. + * + * @author tonytv + */ +@Beta +public class SecurityFilterInvoker implements FilterInvoker { + + /** + * Returns the servlet request to be used in any servlets invoked after this. + */ + @Override + public HttpServletRequest invokeRequestFilterChain(RequestFilter requestFilterChain, + URI uri, HttpServletRequest httpRequest, + ResponseHandler responseHandler) { + + SecurityRequestFilterChain securityChain = cast(SecurityRequestFilterChain.class, requestFilterChain). + orElseThrow(SecurityFilterInvoker::newUnsupportedOperationException); + + ServletRequest wrappedRequest = new ServletRequest(httpRequest, uri); + securityChain.filter(new ServletFilterRequest(wrappedRequest), responseHandler); + return wrappedRequest; + } + + @Override + public void invokeResponseFilterChain( + ResponseFilter responseFilterChain, + URI uri, + HttpServletRequest request, + HttpServletResponse response) { + + SecurityResponseFilterChain securityChain = cast(SecurityResponseFilterChain.class, responseFilterChain). + orElseThrow(SecurityFilterInvoker::newUnsupportedOperationException); + + ServletFilterResponse wrappedResponse = new ServletFilterResponse(new ServletResponse(response)); + securityChain.filter(new ServletRequestView(uri, request), wrappedResponse); + } + + private static UnsupportedOperationException newUnsupportedOperationException() { + return new UnsupportedOperationException( + "Filter type not supported. If a request is handled by servlets or jax-rs, then any filters invoked for that request must be security filters."); + } + + private <T> Optional<T> cast(Class<T> securityFilterChainClass, Object filter) { + return (securityFilterChainClass.isInstance(filter))? + Optional.of(securityFilterChainClass.cast(filter)): + Optional.empty(); + } + + private static class ServletRequestView implements RequestView { + private final HttpServletRequest request; + private final URI uri; + + public ServletRequestView(URI uri, HttpServletRequest request) { + this.request = request; + this.uri = uri; + } + + @Override + public Object getAttribute(String name) { + return request.getAttribute(name); + } + + @Override + public Optional<Method> getMethod() { + return Optional.of(Method.valueOf(request.getMethod())); + } + + @Override + public URI getUri() { + return uri; + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java new file mode 100644 index 00000000000..77ee10111be --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface SecurityRequestFilter extends RequestFilterBase { + + void filter(DiscFilterRequest request, ResponseHandler handler); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java new file mode 100644 index 00000000000..d6c5629d6c1 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java @@ -0,0 +1,78 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +import com.yahoo.jdisc.http.HttpRequest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Implementation of TypedFilterChain for DiscFilterRequest + * + * @author tejalk + * + */ +public final class SecurityRequestFilterChain extends AbstractResource implements RequestFilter { + + private final List<SecurityRequestFilter> filters = new ArrayList<SecurityRequestFilter>(); + + private SecurityRequestFilterChain(Iterable<? extends SecurityRequestFilter> filters) { + for (SecurityRequestFilter filter : filters) { + this.filters.add(filter); + } + } + + @Override + public void filter(HttpRequest request, ResponseHandler responseHandler) { + DiscFilterRequest discFilterRequest = new JdiscFilterRequest(request); + filter(discFilterRequest, responseHandler); + } + + public void filter(DiscFilterRequest request, ResponseHandler responseHandler) { + ResponseHandlerGuard guard = new ResponseHandlerGuard(responseHandler); + for (int i = 0, len = filters.size(); i < len && !guard.isDone(); ++i) { + filters.get(i).filter(request, guard); + } + } + + public static RequestFilter newInstance(SecurityRequestFilter... filters) { + return newInstance(Arrays.asList(filters)); + } + + public static RequestFilter newInstance(List<? extends SecurityRequestFilter> filters) { + return new SecurityRequestFilterChain(filters); + } + + private static class ResponseHandlerGuard implements ResponseHandler { + + private final ResponseHandler responseHandler; + private boolean done = false; + + public ResponseHandlerGuard(ResponseHandler handler) { + this.responseHandler = handler; + } + + @Override + public ContentChannel handleResponse(Response response) { + done = true; + return responseHandler.handleResponse(response); + } + + public boolean isDone() { + return done; + } + } + + /** Returns an unmodifiable viuew of the filters in this */ + public List<SecurityRequestFilter> getFilters() { + return Collections.unmodifiableList(filters); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java new file mode 100644 index 00000000000..e4acb3f1c89 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +public interface SecurityResponseFilter extends ResponseFilterBase { + + void filter(DiscFilterResponse response, RequestView request); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java new file mode 100644 index 00000000000..6ac68cee894 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java @@ -0,0 +1,89 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; + +/** + * Implementation of TypedFilterChain for DiscFilterResponse + * @author tejalk + * + */ +public class SecurityResponseFilterChain extends AbstractResource implements ResponseFilter { + + private final List<SecurityResponseFilter> filters = new ArrayList<>(); + + private SecurityResponseFilterChain(Iterable<? extends SecurityResponseFilter> filters) { + for (SecurityResponseFilter filter : filters) { + this.filters.add(filter); + } + } + + @Override + public void filter(Response response, Request request) { + if(response instanceof HttpResponse) { + DiscFilterResponse discFilterResponse = new JdiscFilterResponse((HttpResponse)response); + RequestView requestView = new RequestViewImpl(request); + filter(requestView, discFilterResponse); + } + + } + + public void filter(RequestView requestView, DiscFilterResponse response) { + for (SecurityResponseFilter filter : filters) { + filter.filter(response, requestView); + } + } + + public static ResponseFilter newInstance(SecurityResponseFilter... filters) { + return newInstance(Arrays.asList(filters)); + } + + public static ResponseFilter newInstance(List<? extends SecurityResponseFilter> filters) { + return new SecurityResponseFilterChain(filters); + } + + /** Returns an unmodifiable view of the filters in this */ + public List<SecurityResponseFilter> getFilters() { + return Collections.unmodifiableList(filters); + } + + private static class RequestViewImpl implements RequestView { + + private final Request request; + private final Optional<HttpRequest.Method> method; + + public RequestViewImpl(Request request) { + this.request = request; + method = request instanceof HttpRequest ? + Optional.of(((HttpRequest) request).getMethod()): + Optional.empty(); + } + + @Override + public Object getAttribute(String name) { + return request.context().get(name); + } + + @Override + public Optional<HttpRequest.Method> getMethod() { + return method; + } + + @Override + public URI getUri() { + return request.getUri(); + } + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java new file mode 100644 index 00000000000..8b5e91e0ad6 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java @@ -0,0 +1,149 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletRequest; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Servlet implementation for JDisc filter requests. + * + * @since 5.27 + */ +class ServletFilterRequest extends DiscFilterRequest { + + private final ServletRequest parent; + + public ServletFilterRequest(ServletRequest parent) { + super(parent); + this.parent = parent; + } + + ServletRequest getServletRequest() { + return parent; + } + + public void setUri(URI uri) { + parent.setUri(uri); + } + + @Override + public String getMethod() { + return parent.getRequest().getMethod(); + } + + @Override + public void setRemoteAddr(String remoteIpAddress) { + throw new UnsupportedOperationException( + "Setting remote address is not supported for " + this.getClass().getName()); + } + + @Override + public Enumeration<String> getAttributeNames() { + Set<String> names = new HashSet<>(Collections.list(super.getAttributeNames())); + names.addAll(Collections.list(parent.getRequest().getAttributeNames())); + return Collections.enumeration(names); + } + + @Override + public Object getAttribute(String name) { + Object jdiscAttribute = super.getAttribute(name); + return jdiscAttribute != null ? + jdiscAttribute : + parent.getRequest().getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + super.setAttribute(name, value); + parent.getRequest().setAttribute(name, value); + } + + @Override + public boolean containsAttribute(String name) { + return super.containsAttribute(name) + || parent.getRequest().getAttribute(name) != null; + } + + @Override + public void removeAttribute(String name) { + super.removeAttribute(name); + parent.getRequest().removeAttribute(name); + } + + @Override + public String getParameter(String name) { + return parent.getParameter(name); + } + + @Override + public Enumeration<String> getParameterNames() { + return parent.getParameterNames(); + } + + @Override + public void addHeader(String name, String value) { + parent.addHeader(name, value); + } + + @Override + public String getHeader(String name) { + return parent.getHeader(name); + } + + @Override + public Enumeration<String> getHeaderNames() { + return parent.getHeaderNames(); + } + + public List<String> getHeaderNamesAsList() { + return Collections.list(getHeaderNames()); + } + + @Override + public Enumeration<String> getHeaders(String name) { + return parent.getHeaders(name); + } + + @Override + public List<String> getHeadersAsList(String name) { + return Collections.list(getHeaders(name)); + } + + @Override + public void setHeaders(String name, String value) { + parent.setHeaders(name, value); + } + + @Override + public void setHeaders(String name, List<String> values) { + parent.setHeaders(name, values); + } + + @Override + public void removeHeaders(String name) { + parent.removeHeaders(name); + } + + @Override + public void clearCookies() { + parent.removeHeaders(HttpHeaders.Names.COOKIE); + } + + @Override + public void setCharacterEncoding(String encoding) { + super.setCharacterEncoding(encoding); + try { + parent.setCharacterEncoding(encoding); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Encoding not supported: " + encoding, e); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java new file mode 100644 index 00000000000..13f3eb828cd --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java @@ -0,0 +1,85 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.google.common.collect.Iterables; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletResponse; + +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Servlet implementation for JDisc filter responses. + * + * @since 5.27 + */ +class ServletFilterResponse extends DiscFilterResponse { + + private final ServletResponse parent; + + public ServletFilterResponse(ServletResponse parent) { + super(parent); + this.parent = parent; + } + + ServletResponse getServletResponse() { + return parent; + } + + public void setStatus(int status) { + parent.setStatus(status); + } + + @Override + public void setHeader(String name, String value) { + parent.setHeader(name, value); + } + + @Override + public void removeHeaders(String name) { + HttpServletResponse parentResponse = parent.getResponse(); + if (parentResponse instanceof org.eclipse.jetty.server.Response) { + org.eclipse.jetty.server.Response jettyResponse = (org.eclipse.jetty.server.Response)parentResponse; + jettyResponse.getHttpFields().remove(name); + } else { + throw new UnsupportedOperationException( + "Cannot remove headers for response of type " + parentResponse.getClass().getName()); + } + } + + // Why have a setHeaders that takes a single string? + @Override + public void setHeaders(String name, String value) { + parent.setHeader(name, value); + } + + @Override + public void setHeaders(String name, List<String> values) { + for (String value : values) + parent.addHeader(name, value); + } + + @Override + public void addHeader(String name, String value) { + parent.addHeader(name, value); + } + + @Override + public String getHeader(String name) { + Collection<String> headers = parent.getHeaders(name); + return headers.isEmpty() + ? null + : Iterables.getLast(headers); + } + + @Override + public void setCookies(List<Cookie> cookies) { + removeHeaders(HttpHeaders.Names.SET_COOKIE); + for (Cookie cookie : cookies) { + addHeader(HttpHeaders.Names.SET_COOKIE, Cookie.toSetCookieHeader(Arrays.asList(cookie))); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java new file mode 100644 index 00000000000..9cb103f0c6b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public final class EmptyRequestFilter extends NoopSharedResource implements RequestFilter { + + public static final RequestFilter INSTANCE = new EmptyRequestFilter(); + + private EmptyRequestFilter() { + // hide + } + + @Override + public void filter(HttpRequest request, ResponseHandler handler) { + + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java new file mode 100644 index 00000000000..7c09e605b46 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public final class EmptyResponseFilter extends NoopSharedResource implements ResponseFilter { + + public static final ResponseFilter INSTANCE = new EmptyResponseFilter(); + + private EmptyResponseFilter() { + // hide + } + + @Override + public void filter(Response response, Request request) { + + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java new file mode 100644 index 00000000000..76ab390f259 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.application.ResourcePool; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public final class RequestFilterChain extends AbstractResource implements RequestFilter { + + private final List<RequestFilter> filters = new ArrayList<>(); + private final ResourcePool filterReferences = new ResourcePool(); + + private RequestFilterChain(Iterable<? extends RequestFilter> filters) { + for (RequestFilter filter : filters) { + this.filters.add(filter); + filterReferences.retain(filter); + } + } + + @Override + public void filter(HttpRequest request, ResponseHandler responseHandler) { + ResponseHandlerGuard guard = new ResponseHandlerGuard(responseHandler); + for (int i = 0, len = filters.size(); i < len && !guard.isDone(); ++i) { + filters.get(i).filter(request, guard); + } + } + + @Override + protected void destroy() { + filterReferences.release(); + } + + public static RequestFilter newInstance(RequestFilter... filters) { + return newInstance(Arrays.asList(filters)); + } + + public static RequestFilter newInstance(List<? extends RequestFilter> filters) { + if (filters.size() == 0) { + return EmptyRequestFilter.INSTANCE; + } + if (filters.size() == 1) { + return filters.get(0); + } + return new RequestFilterChain(filters); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java new file mode 100644 index 00000000000..1433b98006f --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ResourcePool; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class ResponseFilterChain extends AbstractResource implements ResponseFilter { + + private final List<ResponseFilter> filters = new ArrayList<>(); + private final ResourcePool filterReferences = new ResourcePool(); + + private ResponseFilterChain(Iterable<? extends ResponseFilter> filters) { + for (ResponseFilter filter : filters) { + this.filters.add(filter); + filterReferences.retain(filter); + } + } + + @Override + public void filter(Response response, Request request) { + for (ResponseFilter filter : filters) { + filter.filter(response, request); + } + } + + @Override + protected void destroy() { + filterReferences.release(); + } + + public static ResponseFilter newInstance(ResponseFilter... filters) { + return newInstance(Arrays.asList(filters)); + } + + public static ResponseFilter newInstance(List<? extends ResponseFilter> filters) { + if (filters.size() == 0) { + return EmptyResponseFilter.INSTANCE; + } + if (filters.size() == 1) { + return filters.get(0); + } + return new ResponseFilterChain(filters); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java new file mode 100644 index 00000000000..5194cafd527 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +final class ResponseHandlerGuard implements ResponseHandler { + + private final ResponseHandler responseHandler; + private boolean done = false; + + public ResponseHandlerGuard(ResponseHandler handler) { + this.responseHandler = handler; + } + + @Override + public ContentChannel handleResponse(Response response) { + done = true; + return responseHandler.handleResponse(response); + } + + public boolean isDone() { + return done; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java new file mode 100644 index 00000000000..c37c4a3047b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java new file mode 100644 index 00000000000..551ad0aad87 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@PublicApi +@ExportPackage +package com.yahoo.jdisc.http.filter; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java new file mode 100644 index 00000000000..d4b2709e47b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@PublicApi +@ExportPackage +package com.yahoo.jdisc.http; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java new file mode 100644 index 00000000000..cc3c4efc913 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server; + +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class FilterBindings { + + private final BindingRepository<RequestFilter> requestFilters; + private final BindingRepository<ResponseFilter> responseFilters; + + public FilterBindings(BindingRepository<RequestFilter> requestFilters, + BindingRepository<ResponseFilter> responseFilters) { + this.requestFilters = requestFilters; + this.responseFilters = responseFilters; + } + + public BindingRepository<RequestFilter> getRequestFilters() { + return requestFilters; + } + + public BindingRepository<ResponseFilter> getResponseFilters() { + return responseFilters; + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java new file mode 100644 index 00000000000..1049c2eed61 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java @@ -0,0 +1,150 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Objects; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.container.logging.AccessLogEntry; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.RequestLog; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.component.AbstractLifeCycle; + +import javax.servlet.http.HttpServletRequest; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is a bridge between Jetty's {@link org.eclipse.jetty.server.handler.RequestLogHandler} + * and our own configurable access logging in different formats provided by {@link AccessLog}. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class AccessLogRequestLog extends AbstractLifeCycle implements RequestLog { + + private static final Logger logger = Logger.getLogger(AccessLogRequestLog.class.getName()); + + private static final String HEADER_NAME_Y_RA = "y-ra"; + private static final String HEADER_NAME_Y_RP = "y-rp"; + private static final String HEADER_NAME_YAHOOREMOTEIP = "yahooremoteip"; + private static final String HEADER_NAME_X_FORWARDED_FOR = "x-forwarded-for"; + private static final String HEADER_NAME_CLIENT_IP = "client-ip"; + + private final AccessLog accessLog; + + public AccessLogRequestLog(final AccessLog accessLog) { + this.accessLog = accessLog; + } + + @Override + public void log(final Request request, final Response response) { + final AccessLogEntry accessLogEntryFromServletRequest = (AccessLogEntry) request.getAttribute( + JDiscHttpServlet.ATTRIBUTE_NAME_ACCESS_LOG_ENTRY); + final AccessLogEntry accessLogEntry; + if (accessLogEntryFromServletRequest != null) { + accessLogEntry = accessLogEntryFromServletRequest; + } else { + accessLogEntry = new AccessLogEntry(); + populateAccessLogEntryFromHttpServletRequest(request, accessLogEntry); + } + + final long startTime = request.getTimeStamp(); + final long endTime = System.currentTimeMillis(); + accessLogEntry.setTimeStamp(startTime); + accessLogEntry.setDurationBetweenRequestResponse(endTime - startTime); + accessLogEntry.setReturnedContentSize(response.getContentCount()); + accessLogEntry.setStatusCode(response.getStatus()); + + accessLog.log(accessLogEntry); + } + + /* + * Collecting all log entry population based on extracting information from HttpServletRequest in one method + * means that this may easily be moved to another location, e.g. if we want to populate this at instantiation + * time rather than at logging time. We may, for example, want to set things such as http headers and ip + * addresses up-front and make it illegal for request handlers to modify these later. + */ + public static void populateAccessLogEntryFromHttpServletRequest( + final HttpServletRequest request, + final AccessLogEntry accessLogEntry) { + final String quotedPath = request.getRequestURI(); + final String quotedQuery = request.getQueryString(); + try { + final StringBuilder uriBuffer = new StringBuilder(); + uriBuffer.append(quotedPath); + if (quotedQuery != null) { + uriBuffer.append('?').append(quotedQuery); + } + final URI uri = new URI(uriBuffer.toString()); + accessLogEntry.setURI(uri); + } catch (URISyntaxException e) { + setUriFromMalformedInput(accessLogEntry, quotedPath, quotedQuery); + } + + final String remoteAddress = getRemoteAddress(request); + final int remotePort = getRemotePort(request); + final String peerAddress = request.getRemoteAddr(); + final int peerPort = request.getRemotePort(); + + accessLogEntry.setUserAgent(request.getHeader("User-Agent")); + accessLogEntry.setHttpMethod(request.getMethod()); + accessLogEntry.setHostString(request.getHeader("Host")); + accessLogEntry.setReferer(request.getHeader("Referer")); + accessLogEntry.setIpV4Address(peerAddress); + accessLogEntry.setRemoteAddress(remoteAddress); + accessLogEntry.setRemotePort(remotePort); + if (!Objects.equal(remoteAddress, peerAddress)) { + accessLogEntry.setPeerAddress(peerAddress); + } + if (remotePort != peerPort) { + accessLogEntry.setPeerPort(peerPort); + } + accessLogEntry.setHttpVersion(request.getProtocol()); + } + + private static String getRemoteAddress(final HttpServletRequest request) { + return Alternative.preferred(request.getHeader(HEADER_NAME_Y_RA)) + .alternatively(() -> request.getHeader(HEADER_NAME_YAHOOREMOTEIP)) + .alternatively(() -> request.getHeader(HEADER_NAME_X_FORWARDED_FOR)) + .alternatively(() -> request.getHeader(HEADER_NAME_CLIENT_IP)) + .orElseGet(request::getRemoteAddr); + } + + private static int getRemotePort(final HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(HEADER_NAME_Y_RP)) + .map(Integer::valueOf) + .orElseGet(request::getRemotePort); + } + + private static void setUriFromMalformedInput(final AccessLogEntry accessLogEntry, final String quotedPath, final String quotedQuery) { + try { + final String scheme = null; + final String authority = null; + final String fragment = null; + final URI uri = new URI(scheme, authority, unquote(quotedPath), unquote(quotedQuery), fragment); + accessLogEntry.setURI(uri); + } catch (URISyntaxException e) { + // I have no idea how this can happen here now... + logger.log(Level.WARNING, "Could not convert String URI to URI object", e); + } + } + + private static String unquote(final String quotedQuery) { + if (quotedQuery == null) { + return null; + } + try { + // inconsistent handling of semi-colon added here... + return URLDecoder.decode(quotedQuery, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + return quotedQuery; + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java new file mode 100644 index 00000000000..2d9e0558455 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +import java.util.Map; +import java.util.Optional; + +/** + * A wrapper RequestHandler that enables access logging. By wrapping the request handler, we are able to wrap the + * response handler as well. Hence, we can populate the access log entry with information from both the request + * and the response. This wrapper also adds the access log entry to the request context, so that request handlers + * may add information to it. + * + * Does not otherwise interfere with the request processing of the delegate request handler. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + * $Id$ + */ +public class AccessLoggingRequestHandler extends AbstractRequestHandler { + public static final String CONTEXT_KEY_ACCESS_LOG_ENTRY + = AccessLoggingRequestHandler.class.getName() + "_access-log-entry"; + + public static Optional<AccessLogEntry> getAccessLogEntry(final HttpRequest jdiscRequest) { + final Map<String, Object> requestContextMap = jdiscRequest.context(); + return getAccessLogEntry(requestContextMap); + } + + public static Optional<AccessLogEntry> getAccessLogEntry(final Map<String, Object> requestContextMap) { + return Optional.ofNullable( + (AccessLogEntry) requestContextMap.get(CONTEXT_KEY_ACCESS_LOG_ENTRY)); + } + + private final RequestHandler delegate; + private final AccessLogEntry accessLogEntry; + + public AccessLoggingRequestHandler( + final RequestHandler delegateRequestHandler, + final AccessLogEntry accessLogEntry) { + this.delegate = delegateRequestHandler; + this.accessLogEntry = accessLogEntry; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request); + final HttpRequest httpRequest = (HttpRequest) request; + httpRequest.context().put(CONTEXT_KEY_ACCESS_LOG_ENTRY, accessLogEntry); + final ResponseHandler accessLoggingResponseHandler = new AccessLoggingResponseHandler(handler, accessLogEntry); + final ContentChannel requestContentChannel = delegate.handleRequest(request, accessLoggingResponseHandler); + return requestContentChannel; + } + + private static class AccessLoggingResponseHandler implements ResponseHandler { + private final ResponseHandler delegateHandler; + private final AccessLogEntry accessLogEntry; + + public AccessLoggingResponseHandler( + final ResponseHandler delegateHandler, + final AccessLogEntry accessLogEntry) { + this.delegateHandler = delegateHandler; + this.accessLogEntry = accessLogEntry; + } + + @Override + public ContentChannel handleResponse(Response response) { + return delegateHandler.handleResponse(response); + } + + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java new file mode 100644 index 00000000000..267a53033ac --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Simple monad class, like Optional but with support for chaining alternatives in preferred order. + * + * Holds a current value (immutably), but if the current value is null provides an easy way to obtain an instance + * with another value, ad infinitum. + * + * Instances of this class are immutable and thread-safe. + * + * @author bakksjo + */ +public class Alternative<T> { + private final T value; + + private Alternative(final T value) { + this.value = value; + } + + /** + * Creates an instance with the supplied value. + */ + public static <T> Alternative<T> preferred(final T value) { + return new Alternative<>(value); + } + + /** + * Returns itself (unchanged) iff current value != null, + * otherwise returns a new instance with the value supplied by the supplier. + */ + public Alternative<T> alternatively(final Supplier<? extends T> supplier) { + if (value != null) { + return this; + } + + return new Alternative<>(supplier.get()); + } + + /** + * Returns the held value iff != null, otherwise invokes the supplier and returns its value. + */ + public T orElseGet(final Supplier<? extends T> supplier) { + if (value != null) { + return value; + } + return supplier.get(); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof Alternative<?>)) { + return false; + } + + final Alternative<?> other = (Alternative<?>) o; + + return Objects.equals(value, other.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java new file mode 100644 index 00000000000..8d974639f47 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import java.io.IOException; + +/** + * Interface for async listeners only interested in onComplete. + * @author tonytv + */ +@FunctionalInterface +interface AsyncCompleteListener extends AsyncListener { + @Override + default void onTimeout(AsyncEvent event) throws IOException {} + + @Override + default void onError(AsyncEvent event) throws IOException {} + + @Override + default void onStartAsync(AsyncEvent event) throws IOException {} +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java new file mode 100644 index 00000000000..874d9ab7173 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java @@ -0,0 +1,350 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ConnectorConfig.Ssl; +import com.yahoo.jdisc.http.ConnectorConfig.Ssl.PemKeyStore; +import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.jdisc.http.ssl.ReaderForPath; +import com.yahoo.jdisc.http.ssl.SslKeyStore; +import com.yahoo.jdisc.http.ssl.SslKeyStoreFactory; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.ConnectorStatistics; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import javax.servlet.ServletRequest; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Field; +import java.net.Socket; +import java.net.SocketException; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ServerSocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.google.common.io.Closeables.closeQuietly; +import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.JKS; +import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM; +import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + * @since 5.21.0 + */ +public class ConnectorFactory { + + private final static Logger log = Logger.getLogger(ConnectorFactory.class.getName()); + private final ConnectorConfig connectorConfig; + private final SslKeyStoreFactory sslKeyStoreFactory; + private final SecretStore secretStore; + + @Inject + public ConnectorFactory(ConnectorConfig connectorConfig, SslKeyStoreFactory sslKeyStoreFactory, SecretStore secretStore) { + this.connectorConfig = connectorConfig; + this.sslKeyStoreFactory = sslKeyStoreFactory; + this.secretStore = secretStore; + + if (connectorConfig.ssl().enabled()) + validateSslConfig(connectorConfig); + } + + // TODO: can be removed when we have dedicated SSL config in services.xml + private static void validateSslConfig(ConnectorConfig config) { + ConnectorConfig.Ssl ssl = config.ssl(); + + if (ssl.keyStoreType() == JKS) { + if (! ssl.pemKeyStore().keyPath().isEmpty() + || ! ssl.pemKeyStore().certificatePath().isEmpty()) + throw new IllegalArgumentException( + "Setting pemKeyStore attributes does not make sense when keyStoreType==JKS."); + } + if (ssl.keyStoreType() == PEM) { + if (! ssl.keyStorePath().isEmpty()) + throw new IllegalArgumentException( + "Setting keyStorePath does not make sense when keyStoreType==PEM"); + } + } + + public ConnectorConfig getConnectorConfig() { + return connectorConfig; + } + + public ServerConnector createConnector(final Metric metric, final Server server, final ServerSocketChannel ch, Map<Path, FileChannel> keyStoreChannels) { + final ServerConnector connector; + if (connectorConfig.ssl().enabled()) { + connector = new JDiscServerConnector(connectorConfig, metric, server, ch, + newSslConnectionFactory(keyStoreChannels), + newHttpConnectionFactory()); + } else { + connector = new JDiscServerConnector(connectorConfig, metric, server, ch, + newHttpConnectionFactory()); + } + connector.setPort(connectorConfig.listenPort()); + connector.setName(connectorConfig.name()); + connector.setAcceptQueueSize(connectorConfig.acceptQueueSize()); + connector.setReuseAddress(connectorConfig.reuseAddress()); + connector.setSoLingerTime(connectorConfig.soLingerTime()); + connector.setIdleTimeout((long)(connectorConfig.idleTimeout() * 1000.0)); + connector.setStopTimeout((long)(connectorConfig.stopTimeout() * 1000.0)); + return connector; + } + + private HttpConnectionFactory newHttpConnectionFactory() { + final HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSendDateHeader(true); + httpConfig.setSendServerVersion(false); + httpConfig.setSendXPoweredBy(false); + httpConfig.setHeaderCacheSize(connectorConfig.headerCacheSize()); + httpConfig.setOutputBufferSize(connectorConfig.outputBufferSize()); + httpConfig.setRequestHeaderSize(connectorConfig.requestHeaderSize()); + httpConfig.setResponseHeaderSize(connectorConfig.responseHeaderSize()); + if (connectorConfig.ssl().enabled()) { + httpConfig.addCustomizer(new SecureRequestCustomizer()); + } + return new HttpConnectionFactory(httpConfig); + } + + //TODO: does not support loading non-yahoo readable JKS key stores. + private SslConnectionFactory newSslConnectionFactory(Map<Path, FileChannel> keyStoreChannels) { + Ssl sslConfig = connectorConfig.ssl(); + + final SslContextFactory factory = new SslContextFactory(); + if (!sslConfig.excludeProtocol().isEmpty()) { + final String[] prots = new String[sslConfig.excludeProtocol().size()]; + for (int i = 0; i < prots.length; i++) { + prots[i] = sslConfig.excludeProtocol(i).name(); + } + factory.setExcludeProtocols(prots); + } + if (!sslConfig.includeProtocol().isEmpty()) { + final String[] prots = new String[sslConfig.includeProtocol().size()]; + for (int i = 0; i < prots.length; i++) { + prots[i] = sslConfig.includeProtocol(i).name(); + } + factory.setIncludeProtocols(prots); + } + if (!sslConfig.excludeCipherSuite().isEmpty()) { + final String[] ciphs = new String[sslConfig.excludeCipherSuite().size()]; + for (int i = 0; i < ciphs.length; i++) { + ciphs[i] = sslConfig.excludeCipherSuite(i).name(); + } + factory.setExcludeCipherSuites(ciphs); + + } + if (!sslConfig.includeCipherSuite().isEmpty()) { + final String[] ciphs = new String[sslConfig.includeCipherSuite().size()]; + for (int i = 0; i < ciphs.length; i++) { + ciphs[i] = sslConfig.includeCipherSuite(i).name(); + } + factory.setIncludeCipherSuites(ciphs); + + } + + + Optional<String> password = Optional.of(sslConfig.keyDbKey()). + filter(key -> !key.isEmpty()).map(secretStore::getSecret); + + switch (sslConfig.keyStoreType()) { + case PEM: + factory.setKeyStore(getKeyStore(sslConfig.pemKeyStore(), keyStoreChannels)); + if (password.isPresent()) { + log.warning("Encrypted PEM key stores are not supported."); + } + break; + case JKS: + factory.setKeyStorePath(sslConfig.keyStorePath()); + factory.setKeyStoreType(sslConfig.keyStoreType().toString()); + factory.setKeyStorePassword(password.orElseThrow(passwordRequiredForJKSKeyStore("key"))); + break; + } + + if (!sslConfig.trustStorePath().isEmpty()) { + factory.setTrustStorePath(sslConfig.trustStorePath()); + factory.setTrustStoreType(sslConfig.trustStoreType().toString()); + factory.setTrustStorePassword(password.orElseThrow(passwordRequiredForJKSKeyStore("trust"))); + } + + factory.setSslKeyManagerFactoryAlgorithm(sslConfig.sslKeyManagerFactoryAlgorithm()); + factory.setProtocol(sslConfig.protocol()); + return new SslConnectionFactory(factory, HttpVersion.HTTP_1_1.asString()); + } + + @SuppressWarnings("ThrowableInstanceNeverThrown") + private Supplier<RuntimeException> passwordRequiredForJKSKeyStore(String type) { + return () -> new RuntimeException(String.format("Password is required for JKS %s store", type)); + } + + private KeyStore getKeyStore(PemKeyStore pemKeyStore, Map<Path, FileChannel> keyStoreChannels) { + Preconditions.checkArgument(!pemKeyStore.certificatePath().isEmpty(), "Missing certificate path."); + Preconditions.checkArgument(!pemKeyStore.keyPath().isEmpty(), "Missing key path."); + + class KeyStoreReaderForPath implements AutoCloseable { + private final Optional<FileChannel> channel; + public final ReaderForPath readerForPath; + + + KeyStoreReaderForPath(String pathString) { + Path path = Paths.get(pathString); + channel = Optional.ofNullable(keyStoreChannels.get(path)); + readerForPath = new ReaderForPath( + channel.map(this::getReader).orElseGet(() -> getReader(path)), + path); + } + + private Reader getReader(FileChannel channel) { + try { + channel.position(0); + return Channels.newReader(channel, StandardCharsets.UTF_8.newDecoder(), -1); + } catch (IOException e) { + throw throwUnchecked(e); + } + + } + + private Reader getReader(Path path) { + try { + return Files.newBufferedReader(path); + } catch (IOException e) { + throw new RuntimeException("Failed opening " + path, e); + } + } + + @Override + public void close() { + //channels are reused + if (!channel.isPresent()) { + closeQuietly(readerForPath.reader); + } + } + } + + try (KeyStoreReaderForPath certificateReader = new KeyStoreReaderForPath(pemKeyStore.certificatePath()); + KeyStoreReaderForPath keyReader = new KeyStoreReaderForPath(pemKeyStore.keyPath())) { + SslKeyStore keyStore = sslKeyStoreFactory.createKeyStore(certificateReader.readerForPath, + keyReader.readerForPath); + return keyStore.loadJavaKeyStore(); + } catch (Exception e) { + throw new RuntimeException("Failed setting up key store for " + pemKeyStore.keyPath() + ", " + pemKeyStore.certificatePath(), e); + } + } + + public static class JDiscServerConnector extends ServerConnector { + public static final String REQUEST_ATTRIBUTE = JDiscServerConnector.class.getName(); + private final static Logger log = Logger.getLogger(JDiscServerConnector.class.getName()); + private final Metric.Context metricCtx; + private final ConnectorStatistics statistics; + private final boolean tcpKeepAlive; + private final boolean tcpNoDelay; + private final ServerSocketChannel channelOpenedByActivator; + + private JDiscServerConnector( + final ConnectorConfig config, + final Metric metric, + final Server server, + final ServerSocketChannel channelOpenedByActivator, + final ConnectionFactory... factories) { + super(server, factories); + this.channelOpenedByActivator = channelOpenedByActivator; + this.tcpKeepAlive = config.tcpKeepAliveEnabled(); + this.tcpNoDelay = config.tcpNoDelay(); + this.metricCtx = createMetricContext(config, metric); + + this.statistics = new ConnectorStatistics(); + addBean(statistics); + } + + private Metric.Context createMetricContext(ConnectorConfig config, Metric metric) { + Map<String, Object> props = new TreeMap<>(); + props.put(JettyHttpServer.Metrics.NAME_DIMENSION, config.name()); + props.put(JettyHttpServer.Metrics.PORT_DIMENSION, config.listenPort()); + return metric.createContext(props); + } + + @Override + protected void configure(final Socket socket) { + super.configure(socket); + try { + socket.setKeepAlive(tcpKeepAlive); + socket.setTcpNoDelay(tcpNoDelay); + } catch (final SocketException ignored) { + + } + } + + @Override + public void open() throws IOException { + if (channelOpenedByActivator == null) { + log.log(Level.INFO, "No channel set by activator, opening channel ourselves."); + try { + super.open(); + } catch (RuntimeException e) { + log.log(Level.SEVERE, "failed org.eclipse.jetty.server.Server open() with port "+getPort()); + throw e; + } + return; + } + log.log(Level.INFO, "Using channel set by activator: " + channelOpenedByActivator); + + channelOpenedByActivator.socket().setReuseAddress(getReuseAddress()); + int localPort = channelOpenedByActivator.socket().getLocalPort(); + try { + uglySetLocalPort(localPort); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Could not set local port.", e); + } + if (localPort <= 0) { + throw new IOException("Server channel not bound"); + } + addBean(channelOpenedByActivator); + channelOpenedByActivator.configureBlocking(true); + addBean(channelOpenedByActivator); + + try { + uglySetChannel(channelOpenedByActivator); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Could not set server channel.", e); + } + } + + private void uglySetLocalPort(int localPort) throws NoSuchFieldException, IllegalAccessException { + Field localPortField = ServerConnector.class.getDeclaredField("_localPort"); + localPortField.setAccessible(true); + localPortField.set(this, localPort); + } + + private void uglySetChannel(ServerSocketChannel channelOpenedByActivator) throws NoSuchFieldException, IllegalAccessException { + Field acceptChannelField = ServerConnector.class.getDeclaredField("_acceptChannel"); + acceptChannelField.setAccessible(true); + acceptChannelField.set(this, channelOpenedByActivator); + } + + public ConnectorStatistics getStatistics() { return statistics; } + + public Metric.Context getMetricContext() { return metricCtx; } + + public static JDiscServerConnector fromRequest(ServletRequest request) { + return (JDiscServerConnector)request.getAttribute(REQUEST_ATTRIBUTE); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java new file mode 100644 index 00000000000..2b28d866f2f --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java @@ -0,0 +1,59 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +/** + * A wrapper to make exceptions leaking into Jetty easier to track. Jetty + * swallows all information about where an exception was thrown, so this wrapper + * ensures some extra information is automatically added to the contents of + * getMessage(). + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class ExceptionWrapper extends RuntimeException { + private final String message; + + /** + * Update if serializable contents are added. + */ + private static final long serialVersionUID = 1L; + + public ExceptionWrapper(Throwable t) { + super(t); + this.message = formatMessage(t); + } + + // If calling methods from the constructor, it makes life easier if the + // methods are static... + private static String formatMessage(final Throwable t) { + StringBuilder b = new StringBuilder(); + Throwable cause = t; + while (cause != null) { + StackTraceElement[] trace = cause.getStackTrace(); + String currentMsg = cause.getMessage(); + + if (b.length() > 0) { + b.append(": "); + } + b.append(t.getClass().getSimpleName()).append('('); + if (currentMsg != null) { + b.append('"').append(currentMsg).append('"'); + } + b.append(')'); + if (trace.length > 0) { + b.append(" at ").append(trace[0].getClassName()).append('('); + if (trace[0].getFileName() != null) { + b.append(trace[0].getFileName()).append(':') + .append(trace[0].getLineNumber()); + } + b.append(')'); + } + cause = cause.getCause(); + } + return b.toString(); + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java new file mode 100644 index 00000000000..3c7908356d4 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +/** + * Utility methods for exceptions + * + * @author tonytv + */ +public class Exceptions { + + /** + * Allows treating checked exceptions as unchecked. + * Usage: + * throw throwUnchecked(e); + * The reason for the return type is to allow writing throw at the call site + * instead of just calling throwUnchecked. Just calling throwUnchecked + * means that the java compiler won't know that the statement will throw an exception, + * and will therefore complain on things such e.g. missing return value. + */ + public static RuntimeException throwUnchecked(Throwable e) { + throwUncheckedImpl(e); + return null; + } + + @SuppressWarnings("unchecked") + private static <T extends Throwable> void throwUncheckedImpl(Throwable t) throws T { + throw (T)t; + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java new file mode 100644 index 00000000000..ef8698ff4f1 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.ImplementedBy; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; + +/** + * Separate interface since DiscFilterRequest/Response and Security filter chains are not accessible in this bundle + */ +@ImplementedBy(UnsupportedFilterInvoker.class) +public interface FilterInvoker { + HttpServletRequest invokeRequestFilterChain(RequestFilter requestFilterChain, + URI uri, + HttpServletRequest httpRequest, + ResponseHandler responseHandler); + + void invokeResponseFilterChain( + ResponseFilter responseFilterChain, + URI uri, + HttpServletRequest request, + HttpServletResponse response); +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java new file mode 100644 index 00000000000..d787b7294b2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java @@ -0,0 +1,266 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.Locale; + +/** + * Invokes the response filter the first time anything is output to the underlying PrintWriter. + * The filter must be invoked before the first output call since this might cause the response + * to be committed, i.e. locked and potentially put on the wire. + * Any changes to the response after it has been committed might be ignored or cause exceptions. + * @author tonytv + */ +final class FilterInvokingPrintWriter extends PrintWriter { + private final PrintWriter delegate; + private final OneTimeRunnable filterInvoker; + + public FilterInvokingPrintWriter(PrintWriter delegate, OneTimeRunnable filterInvoker) { + /* The PrintWriter class both + * 1) exposes new methods, the PrintWriter "interface" + * 2) implements PrintWriter and Writer methods that does some extra things before calling down to the writer methods. + * If super was invoked with the delegate PrintWriter, the superclass would behave as a PrintWriter(PrintWriter), + * i.e. the extra things in 2. would be done twice. + * To avoid this, all the methods of PrintWriter are overridden with versions that forward directly to the underlying delegate + * instead of going through super. + * The super class is initialized with a non-functioning writer to catch mistakenly non-overridden methods. + */ + super(new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + throwAssertionError(); + } + + private void throwAssertionError() { + throw new AssertionError(FilterInvokingPrintWriter.class.getName() + " failed to delegate to the underlying writer"); + } + + @Override + public void flush() throws IOException { + throwAssertionError(); + } + + @Override + public void close() throws IOException { + throwAssertionError(); + } + }); + + this.delegate = delegate; + this.filterInvoker = filterInvoker; + } + + @Override + public String toString() { + return getClass().getName() + " (" + super.toString() + ")"; + } + + private void runFilterIfFirstInvocation() { + filterInvoker.runIfFirstInvocation(); + } + + @Override + public void flush() { + runFilterIfFirstInvocation(); + delegate.flush(); + } + + @Override + public void close() { + runFilterIfFirstInvocation(); + delegate.close(); + } + + @Override + public boolean checkError() { + return delegate.checkError(); + } + + @Override + public void write(int c) { + runFilterIfFirstInvocation(); + delegate.write(c); + } + + @Override + public void write(char[] buf, int off, int len) { + runFilterIfFirstInvocation(); + delegate.write(buf, off, len); + } + + @Override + public void write(char[] buf) { + runFilterIfFirstInvocation(); + delegate.write(buf); + } + + @Override + public void write(String s, int off, int len) { + runFilterIfFirstInvocation(); + delegate.write(s, off, len); + } + + @Override + public void write(String s) { + runFilterIfFirstInvocation(); + delegate.write(s); + } + + @Override + public void print(boolean b) { + runFilterIfFirstInvocation(); + delegate.print(b); + } + + @Override + public void print(char c) { + runFilterIfFirstInvocation(); + delegate.print(c); + } + + @Override + public void print(int i) { + runFilterIfFirstInvocation(); + delegate.print(i); + } + + @Override + public void print(long l) { + runFilterIfFirstInvocation(); + delegate.print(l); + } + + @Override + public void print(float f) { + runFilterIfFirstInvocation(); + delegate.print(f); + } + + @Override + public void print(double d) { + runFilterIfFirstInvocation(); + delegate.print(d); + } + + @Override + public void print(char[] s) { + runFilterIfFirstInvocation(); + delegate.print(s); + } + + @Override + public void print(String s) { + runFilterIfFirstInvocation(); + delegate.print(s); + } + + @Override + public void print(Object obj) { + runFilterIfFirstInvocation(); + delegate.print(obj); + } + + @Override + public void println() { + runFilterIfFirstInvocation(); + delegate.println(); + } + + @Override + public void println(boolean x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(char x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(int x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(long x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(float x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(double x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(char[] x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(String x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public void println(Object x) { + runFilterIfFirstInvocation(); + delegate.println(x); + } + + @Override + public PrintWriter printf(String format, Object... args) { + runFilterIfFirstInvocation(); + return delegate.printf(format, args); + } + + @Override + public PrintWriter printf(Locale l, String format, Object... args) { + runFilterIfFirstInvocation(); + return delegate.printf(l, format, args); + } + + @Override + public PrintWriter format(String format, Object... args) { + runFilterIfFirstInvocation(); + return delegate.format(format, args); + } + + @Override + public PrintWriter format(Locale l, String format, Object... args) { + runFilterIfFirstInvocation(); + return delegate.format(l, format, args); + } + + @Override + public PrintWriter append(CharSequence csq) { + runFilterIfFirstInvocation(); + return delegate.append(csq); + } + + @Override + public PrintWriter append(CharSequence csq, int start, int end) { + runFilterIfFirstInvocation(); + return delegate.append(csq, start, end); + } + + @Override + public PrintWriter append(char c) { + runFilterIfFirstInvocation(); + return delegate.append(c); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java new file mode 100644 index 00000000000..6a36dbfc6b6 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java @@ -0,0 +1,165 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import java.io.IOException; + +/** + * Invokes the response filter the first time anything is output to the underlying ServletOutputStream. + * The filter must be invoked before the first output call since this might cause the response + * to be committed, i.e. locked and potentially put on the wire. + * Any changes to the response after it has been committed might be ignored or cause exceptions. + * + * @author tonytv + */ +class FilterInvokingServletOutputStream extends ServletOutputStream { + private final ServletOutputStream delegate; + private final OneTimeRunnable filterInvoker; + + public FilterInvokingServletOutputStream(ServletOutputStream delegate, OneTimeRunnable filterInvoker) { + this.delegate = delegate; + this.filterInvoker = filterInvoker; + } + + @Override + public boolean isReady() { + return delegate.isReady(); + } + + @Override + public void setWriteListener(WriteListener writeListener) { + delegate.setWriteListener(writeListener); + } + + + private void runFilterIfFirstInvocation() { + filterInvoker.runIfFirstInvocation(); + } + + @Override + public void write(int b) throws IOException { + runFilterIfFirstInvocation(); + delegate.write(b); + } + + + @Override + public void write(byte[] b) throws IOException { + runFilterIfFirstInvocation(); + delegate.write(b); + } + + @Override + public void print(String s) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(s); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + runFilterIfFirstInvocation(); + delegate.write(b, off, len); + } + + @Override + public void print(boolean b) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(b); + } + + @Override + public void flush() throws IOException { + runFilterIfFirstInvocation(); + delegate.flush(); + } + + @Override + public void print(char c) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(c); + } + + @Override + public void close() throws IOException { + runFilterIfFirstInvocation(); + delegate.close(); + } + + @Override + public void print(int i) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(i); + } + + @Override + public void print(long l) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(l); + } + + @Override + public void print(float f) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(f); + } + + @Override + public void print(double d) throws IOException { + runFilterIfFirstInvocation(); + delegate.print(d); + } + + @Override + public void println() throws IOException { + runFilterIfFirstInvocation(); + delegate.println(); + } + + @Override + public void println(String s) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(s); + } + + @Override + public void println(boolean b) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(b); + } + + @Override + public void println(char c) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(c); + } + + @Override + public void println(int i) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(i); + } + + @Override + public void println(long l) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(l); + } + + @Override + public void println(float f) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(f); + } + + @Override + public void println(double d) throws IOException { + runFilterIfFirstInvocation(); + delegate.println(d); + } + + @Override + public String toString() { + return getClass().getCanonicalName() + " (" + delegate.toString() + ")"; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java new file mode 100644 index 00000000000..b8073bc6ab5 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java @@ -0,0 +1,132 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.BindingNotFoundException; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDeniedException; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.core.CompletionHandlers; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Request handler that invokes request and response filters in addition to the bound request handler. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + * $Id$ + */ +class FilteringRequestHandler extends AbstractRequestHandler { + private static final ContentChannel COMPLETING_CONTENT_CHANNEL = new ContentChannel() { + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + CompletionHandlers.tryComplete(handler); + } + + @Override + public void close(final CompletionHandler handler) { + CompletionHandlers.tryComplete(handler); + } + }; + + private final BindingSet<RequestFilter> requestFilters; + private final BindingSet<ResponseFilter> responseFilters; + + public FilteringRequestHandler( + final BindingSet<RequestFilter> requestFilters, + final BindingSet<ResponseFilter> responseFilters) { + this.requestFilters = requestFilters; + this.responseFilters = responseFilters; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler originalResponseHandler) { + Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request); + Objects.requireNonNull(originalResponseHandler, "responseHandler"); + + final RequestFilter requestFilter = requestFilters.resolve(request.getUri()); + final ResponseFilter responseFilter = responseFilters.resolve(request.getUri()); + // Not using request.connect() here - it adds logic for error handling that we'd rather leave to the framework. + final RequestHandler resolvedRequestHandler = request.container().resolveHandler(request); + + if (resolvedRequestHandler == null) { + throw new BindingNotFoundException(request.getUri()); + } + + final RequestHandler requestHandler = new ReferenceCountingRequestHandler(resolvedRequestHandler); + + final ResponseHandler responseHandler; + if (responseFilter != null) { + responseHandler = new FilteringResponseHandler(originalResponseHandler, responseFilter, request); + } else { + responseHandler = originalResponseHandler; + } + + if (requestFilter != null) { + final InterceptingResponseHandler interceptingResponseHandler + = new InterceptingResponseHandler(responseHandler); + requestFilter.filter(HttpRequest.class.cast(request), interceptingResponseHandler); + if (interceptingResponseHandler.hasProducedResponse()) { + return COMPLETING_CONTENT_CHANNEL; + } + } + + final ContentChannel contentChannel = requestHandler.handleRequest(request, responseHandler); + if (contentChannel == null) { + throw new RequestDeniedException(request); + } + return contentChannel; + } + + private static class FilteringResponseHandler implements ResponseHandler { + private final ResponseHandler delegate; + private final ResponseFilter responseFilter; + private final Request request; + + public FilteringResponseHandler( + final ResponseHandler delegate, + final ResponseFilter responseFilter, + final Request request) { + this.delegate = Objects.requireNonNull(delegate); + this.responseFilter = Objects.requireNonNull(responseFilter); + this.request = request; + } + + @Override + public ContentChannel handleResponse(final Response response) { + responseFilter.filter(response, request); + return delegate.handleResponse(response); + } + } + + private static class InterceptingResponseHandler implements ResponseHandler { + private final ResponseHandler delegate; + private AtomicBoolean hasResponded = new AtomicBoolean(false); + + public InterceptingResponseHandler(final ResponseHandler delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + @Override + public ContentChannel handleResponse(final Response response) { + final ContentChannel content = delegate.handleResponse(response); + hasResponded.set(true); + return content; + } + + public boolean hasProducedResponse() { + return hasResponded.get(); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java new file mode 100644 index 00000000000..b0f336e876c --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java @@ -0,0 +1,190 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.yahoo.jdisc.Response.Status.UNSUPPORTED_MEDIA_TYPE; + +/** + * Request handler that wraps POST requests of application/x-www-form-urlencoded data. + * + * The wrapper defers invocation of the "real" request handler until it has read the request content (body), + * parsed the form parameters and merged them into the request's parameters. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + * $Id$ + */ +class FormPostRequestHandler extends AbstractRequestHandler implements ContentChannel { + private static final CompletionHandler NOOP_COMPLETION_HANDLER = new CompletionHandler() { + @Override public void completed() {} + @Override public void failed(final Throwable t) {} + }; + + private final ByteArrayOutputStream accumulatedRequestContent = new ByteArrayOutputStream(); + private final RequestHandler delegateHandler; + private final String contentCharsetName; + private final boolean removeBody; + + private Charset contentCharset; + private HttpRequest request; + private ResourceReference requestReference; + private ResponseHandler responseHandler; + + /** + * @param delegateHandler the "real" request handler that this handler wraps + * @param contentCharsetName name of the charset to use when interpreting the content data + */ + public FormPostRequestHandler( + final RequestHandler delegateHandler, + final String contentCharsetName, + final boolean removeBody) { + this.delegateHandler = Objects.requireNonNull(delegateHandler); + this.contentCharsetName = Objects.requireNonNull(contentCharsetName); + this.removeBody = removeBody; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler responseHandler) { + Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request); + Objects.requireNonNull(responseHandler, "responseHandler"); + + this.contentCharset = getCharsetByName(contentCharsetName); + this.responseHandler = responseHandler; + this.request = (HttpRequest) request; + this.requestReference = request.refer(); + + return this; + } + + @Override + public void write(final ByteBuffer buf, final CompletionHandler completionHandler) { + assert buf.hasArray(); + accumulatedRequestContent.write(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + completionHandler.completed(); + } + + @Override + public void close(final CompletionHandler completionHandler) { + try (final ResourceReference ref = requestReference) { + final byte[] requestContentBytes = accumulatedRequestContent.toByteArray(); + final String content = new String(requestContentBytes, contentCharset); + completionHandler.completed(); + final Map<String, List<String>> parameterMap = parseFormParameters(content); + mergeParameters(parameterMap, request.parameters()); + final ContentChannel contentChannel = delegateHandler.handleRequest(request, responseHandler); + if (contentChannel != null) { + if (!removeBody) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(requestContentBytes); + contentChannel.write(byteBuffer, NOOP_COMPLETION_HANDLER); + } + contentChannel.close(NOOP_COMPLETION_HANDLER); + } + } + } + + /** + * Looks up a Charset given a charset name. + * + * @param charsetName the name of the charset to look up + * @return a valid Charset for the charset name (never returns null) + * @throws RequestException if the charset name is invalid or unsupported + */ + private static Charset getCharsetByName(final String charsetName) throws RequestException { + try { + final Charset charset = Charset.forName(charsetName); + if (charset == null) { + throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName); + } + return charset; + } catch (final IllegalCharsetNameException |UnsupportedCharsetException e) { + throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName, e); + } + } + + /** + * Parses application/x-www-form-urlencoded data into a map of parameters. + * + * @param formContent raw form content data (body) + * @return map of decoded parameters + */ + private static Map<String, List<String>> parseFormParameters(final String formContent) { + if (formContent.isEmpty()) { + return Collections.emptyMap(); + } + + final Map<String, List<String>> parameterMap = new HashMap<>(); + final String[] params = formContent.split("&"); + for (final String param : params) { + final String[] parts = param.split("="); + final String paramName = urlDecode(parts[0]); + final String paramValue = parts.length > 1 ? urlDecode(parts[1]) : ""; + List<String> currentValues = parameterMap.get(paramName); + if (currentValues == null) { + currentValues = new LinkedList<>(); + parameterMap.put(paramName, currentValues); + } + currentValues.add(paramValue); + } + return parameterMap; + } + + /** + * Percent-decoding method that doesn't throw. + * + * @param encoded percent-encoded data + * @return decoded data + */ + private static String urlDecode(final String encoded) { + try { + // Regardless of the charset used to transfer the request body, + // all percent-escaping of non-ascii characters should use UTF-8 code points. + return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()); + } catch (final UnsupportedEncodingException e) { + // Unfortunately, there is no URLDecoder.decode() method that takes a Charset, so we have to deal + // with this exception. + throw new IllegalStateException("Whoa, JVM doesn't support UTF-8 today.", e); + } + } + + /** + * Merges source parameters into a destination map. + * + * @param source containing the parameters to copy into the destination + * @param destination receiver of parameters, possibly already containing data + */ + private static void mergeParameters( + final Map<String,List<String>> source, + final Map<String,List<String>> destination) { + for (Map.Entry<String, List<String>> entry : source.entrySet()) { + final List<String> destinationValues = destination.get(entry.getKey()); + if (destinationValues != null) { + destinationValues.addAll(entry.getValue()); + } else { + destination.put(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java new file mode 100644 index 00000000000..e9aba0cb6c9 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java @@ -0,0 +1,210 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.Metric.Context; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.BindingNotFoundException; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.OverloadException; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; + +import javax.servlet.AsyncContext; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED; +import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class HttpRequestDispatch { + private static final Logger log = Logger.getLogger(HttpRequestDispatch.class.getName()); + + private final static String CHARSET_ANNOTATION = ";charset="; + + private final JDiscContext jDiscContext; + private final AsyncContext async; + private final HttpServletRequest servletRequest; + + private final ServletResponseController servletResponseController; + private final RequestHandler requestHandler; + private final MetricReporter metricReporter; + + public HttpRequestDispatch( + final JDiscContext jDiscContext, + final AccessLogEntry accessLogEntry, + final Context metricContext, + final HttpServletRequest servletRequest, + final HttpServletResponse servletResponse) throws IOException { + this.jDiscContext = jDiscContext; + + requestHandler = newRequestHandler(jDiscContext, accessLogEntry, servletRequest); + + this.metricReporter = new MetricReporter(jDiscContext.metric, metricContext, + ((org.eclipse.jetty.server.Request) servletRequest).getTimeStamp()); + this.servletRequest = servletRequest; + + this.servletResponseController = new ServletResponseController( + servletResponse, + jDiscContext.janitor, + metricReporter, + jDiscContext.developerMode()); + + this.async = servletRequest.startAsync(); + async.setTimeout(0); + } + + public void dispatch() throws IOException { + final ServletRequestReader servletRequestReader; + try { + servletRequestReader = handleRequest(); + } catch (Throwable throwable) { + servletResponseController.trySendError(throwable); + servletResponseController.finishedFuture().whenComplete((result, exception) -> + completeRequestCallback.accept(null, throwable)); + return; + } + + try { + onError(servletRequestReader.finishedFuture, + servletResponseController::trySendError); + + onError(servletResponseController.finishedFuture(), + servletRequestReader::onError); + + CompletableFuture.allOf(servletRequestReader.finishedFuture, servletResponseController.finishedFuture()) + .whenComplete(completeRequestCallback); + } catch (Throwable throwable) { + log.log(Level.WARNING, "Failed registering finished listeners.", throwable); + } + } + + private BiConsumer<Void, Throwable> completeRequestCallback; + { + AtomicBoolean completeRequestCalled = new AtomicBoolean(false); + HttpRequestDispatch parent = this; //used to avoid binding uninitialized variables + + completeRequestCallback = (result, error) -> { + boolean reportedError = false; + + if (error != null) { + if (!(error instanceof OverloadException || error instanceof BindingNotFoundException)) { + log.log(Level.WARNING, "Request failed: " + parent.servletRequest.getRequestURI(), error); + } + reportedError = true; + parent.metricReporter.failedResponse(); + } else { + parent.metricReporter.successfulResponse(); + } + + + boolean alreadyCalled = completeRequestCalled.getAndSet(true); + if (alreadyCalled) { + AssertionError e = new AssertionError("completeRequest called more than once"); + log.log(Level.WARNING, "Assertion failed.", e); + throw e; + } + + try { + parent.async.complete(); + log.finest(() -> "Request completed successfully: " + parent.servletRequest.getRequestURI()); + } catch (Throwable throwable) { + Level level = reportedError ? Level.FINE: Level.WARNING; + log.log(level, "async.complete failed", throwable); + } + }; + } + + private ServletRequestReader handleRequest() throws IOException { + HttpRequest jdiscRequest = HttpRequestFactory.newJDiscRequest(jDiscContext.container, servletRequest); + final ContentChannel requestContentChannel; + + try (ResourceReference ref = References.fromResource(jdiscRequest)) { + HttpRequestFactory.copyHeaders(servletRequest, jdiscRequest); + requestContentChannel = requestHandler.handleRequest(jdiscRequest, servletResponseController.responseHandler); + } + + ServletInputStream servletInputStream = servletRequest.getInputStream(); + + ServletRequestReader servletRequestReader = + new ServletRequestReader( + servletInputStream, + requestContentChannel, + jDiscContext.janitor, + metricReporter); + + servletInputStream.setReadListener(servletRequestReader); + return servletRequestReader; + } + + private static void onError(CompletableFuture<?> future, Consumer<Throwable> errorHandler) { + future.whenComplete((result, exception) -> { + if (exception != null) { + errorHandler.accept(exception); + } + }); + } + + ContentChannel handleRequestFilterResponse(Response response) { + try { + servletRequest.getInputStream().close(); + ContentChannel responseContentChannel = servletResponseController.responseHandler.handleResponse(response); + servletResponseController.finishedFuture().whenComplete(completeRequestCallback); + return responseContentChannel; + } catch (IOException e) { + throw throwUnchecked(e); + } + } + + + private static RequestHandler newRequestHandler( + final JDiscContext context, + final AccessLogEntry accessLogEntry, + final HttpServletRequest servletRequest) { + final RequestHandler requestHandler = wrapHandlerIfFormPost( + new FilteringRequestHandler(context.requestFilters, context.responseFilters), + servletRequest, context.serverConfig.removeRawPostBodyForWwwUrlEncodedPost()); + + return new AccessLoggingRequestHandler(requestHandler, accessLogEntry); + } + + private static RequestHandler wrapHandlerIfFormPost( + final RequestHandler requestHandler, + final HttpServletRequest servletRequest, + final boolean removeBodyForFormPost) { + if (!servletRequest.getMethod().equals("POST")) { + return requestHandler; + } + final String contentType = servletRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE); + if (contentType == null) { + return requestHandler; + } + if (!contentType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED)) { + return requestHandler; + } + return new FormPostRequestHandler(requestHandler, getCharsetName(contentType), removeBodyForFormPost); + } + + private static String getCharsetName(final String contentType) { + if (!contentType.startsWith(CHARSET_ANNOTATION, APPLICATION_X_WWW_FORM_URLENCODED.length())) { + return StandardCharsets.UTF_8.name(); + } + return contentType.substring(APPLICATION_X_WWW_FORM_URLENCODED.length() + CHARSET_ANNOTATION.length()); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java new file mode 100644 index 00000000000..f1c36ffa80f --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java @@ -0,0 +1,98 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; + +import javax.servlet.http.HttpServletRequest; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Enumeration; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class HttpRequestFactory { + public static HttpRequest newJDiscRequest(final CurrentContainer container, + final HttpServletRequest servletRequest) { + return HttpRequest.newServerRequest( + container, + getUri(servletRequest), + HttpRequest.Method.valueOf(servletRequest.getMethod()), + HttpRequest.Version.fromString(servletRequest.getProtocol()), + new InetSocketAddress(servletRequest.getRemoteAddr(), servletRequest.getRemotePort())); + } + + public static URI getUri(HttpServletRequest servletRequest) { + String query = extraQuote(servletRequest.getQueryString()); + try { + return URI.create(servletRequest.getRequestURL() + (query != null ? '?' + query : "")); + } catch (IllegalArgumentException e) { + throw new RequestException(Response.Status.BAD_REQUEST, "Query violates RFC 2396", e); + } + } + + public static void copyHeaders(final HttpServletRequest from, + final HttpRequest to) { + for (final Enumeration<String> it = from.getHeaderNames(); it.hasMoreElements(); ) { + final String key = it.nextElement(); + for (final Enumeration<String> value = from.getHeaders(key); value.hasMoreElements(); ) { + to.headers().add(key, value.nextElement()); + } + } + } + + private static String extraQuote(String queryString) { + // TODO this is just a stopgap measure, we need some sort of sane URI builder, do we have one? + String washed = null; + if (queryString == null) { + return null; + } + + int toAndIncluding = -1; + for (int i = 0; i < queryString.length(); ++i) { + if (quote(queryString.charAt(i)) != null) { + break; + } + toAndIncluding = i; + } + + if (toAndIncluding != (queryString.length() - 1)) { + StringBuilder w = new StringBuilder(queryString.substring(0, toAndIncluding + 1)); + for (int i = toAndIncluding + 1; i < queryString.length(); ++i) { + String s = quote(queryString.charAt(i)); + if (s == null) { + w.append(queryString.charAt(i)); + } else { + w.append(s); + } + } + washed = w.toString(); + } else { + washed = queryString; + } + return washed; + } + + private static String quote(char c) { + switch(c) { + case '\\': + return "%5C"; + case '^': + return "%5E"; + case '{': + return "%7B"; + case '|': + return "%7C"; + case '}': + return "%7D"; + default: + return null; + } + + } + + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java new file mode 100644 index 00000000000..1f7adbde329 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.util.concurrent.Executor; + +public class JDiscContext { + final BindingSet<RequestFilter> requestFilters; + final BindingSet<ResponseFilter> responseFilters; + final CurrentContainer container; + final Executor janitor; + final Metric metric; + final ServerConfig serverConfig; + + public JDiscContext(BindingSet<RequestFilter> requestFilters, + BindingSet<ResponseFilter> responseFilters, + CurrentContainer container, + Executor janitor, + Metric metric, + ServerConfig serverConfig) { + + this.requestFilters = requestFilters; + this.responseFilters = responseFilters; + this.container = container; + this.janitor = janitor; + this.metric = metric; + this.serverConfig = serverConfig; + } + + public boolean developerMode() { + return serverConfig.developerMode(); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java new file mode 100644 index 00000000000..546f59f53f2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java @@ -0,0 +1,287 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.filter.RequestFilter; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; +import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked; + +/** + * Runs JDisc security filters for Servlets + * This component is split in two due to external dependencies: + * 1) JDiscFilterInvokerFilter, which uses package private methods to support JDisc APIs + * 2) SecurityFilterInvoker, which uses Security filter classes and therefore must reside in jdisc_http_filters + * + * @author tonytv + */ +class JDiscFilterInvokerFilter implements Filter { + private final JDiscContext jDiscContext; + private final FilterInvoker filterInvoker; + + public JDiscFilterInvokerFilter(JDiscContext jDiscContext, + FilterInvoker filterInvoker) { + this.jDiscContext = jDiscContext; + this.filterInvoker = filterInvoker; + } + + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletResponse httpResponse = (HttpServletResponse)response; + + URI uri = HttpRequestFactory.getUri(httpRequest); + + AtomicReference<Boolean> responseReturned = new AtomicReference<>(null); + + HttpServletRequest newRequest = runRequestFilterWithMatchingBinding(responseReturned, uri, httpRequest, httpResponse); + assert newRequest != null; + responseReturned.compareAndSet(null, false); + + if (!responseReturned.get()) { + runChainAndResponseFilters(uri, newRequest, httpResponse, chain); + } + } + + private void runChainAndResponseFilters(URI uri, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + Optional<OneTimeRunnable> responseFilterInvoker = + Optional.ofNullable(jDiscContext.responseFilters.resolve(uri)) + .map(responseFilter -> + new OneTimeRunnable(() -> + filterInvoker.invokeResponseFilterChain(responseFilter, uri, request, response))); + + + HttpServletResponse responseForServlet = responseFilterInvoker + .<HttpServletResponse>map(invoker -> + new FilterInvokingResponseWrapper(response, invoker)) + .orElse(response); + + HttpServletRequest requestForServlet = responseFilterInvoker + .<HttpServletRequest>map(invoker -> + new FilterInvokingRequestWrapper(request, invoker, responseForServlet)) + .orElse(request); + + chain.doFilter(requestForServlet, responseForServlet); + + responseFilterInvoker.ifPresent(invoker -> { + boolean requestHandledSynchronously = !request.isAsyncStarted(); + + if (requestHandledSynchronously) { + invoker.runIfFirstInvocation(); + } + // For async requests, response filters will be invoked on AsyncContext.complete(). + }); + } + + private HttpServletRequest runRequestFilterWithMatchingBinding(AtomicReference<Boolean> responseReturned, URI uri, HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + RequestFilter requestFilter = jDiscContext.requestFilters.resolve(uri); + if (requestFilter == null) + return request; + + ResponseHandler responseHandler = createResponseHandler(responseReturned, request, response); + return filterInvoker.invokeRequestFilterChain(requestFilter, uri, request, responseHandler); + } catch (Exception e) { + throw new RuntimeException("Failed running request filter chain for uri " + uri, e); + } + } + + private ResponseHandler createResponseHandler(AtomicReference<Boolean> responseReturned, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + return jdiscResponse -> { + boolean oldValueWasNull = responseReturned.compareAndSet(null, true); + if (!oldValueWasNull) + throw new RuntimeException("Can't return response from filter asynchronously"); + + HttpRequestDispatch requestDispatch = createRequestDispatch(httpRequest, httpResponse); + return requestDispatch.handleRequestFilterResponse(jdiscResponse); + }; + } + + private HttpRequestDispatch createRequestDispatch(HttpServletRequest request, HttpServletResponse response) { + try { + final AccessLogEntry accessLogEntry = null; // Not used in this context. + return new HttpRequestDispatch(jDiscContext, + accessLogEntry, + getConnector(request).getMetricContext(), + request, response); + } catch (IOException e) { + throw throwUnchecked(e); + } + } + + @Override + public void destroy() {} + + // ServletRequest wrapper that is necessary because we need to wrap AsyncContext. + private static class FilterInvokingRequestWrapper extends HttpServletRequestWrapper { + private final OneTimeRunnable filterInvoker; + private final HttpServletResponse servletResponse; + + public FilterInvokingRequestWrapper( + HttpServletRequest request, + OneTimeRunnable filterInvoker, + HttpServletResponse servletResponse) { + super(request); + this.filterInvoker = filterInvoker; + this.servletResponse = servletResponse; + } + + @Override + public AsyncContext startAsync() { + final AsyncContext asyncContext = super.startAsync(); + return new FilterInvokingAsyncContext(asyncContext, filterInvoker, this, servletResponse); + } + + @Override + public AsyncContext startAsync( + final ServletRequest wrappedRequest, + final ServletResponse wrappedResponse) { + // According to the documentation, the passed request/response parameters here must either + // _be_ or _wrap_ the original request/response objects passed to the servlet - which are + // our wrappers, so no need to wrap again - we can use the user-supplied objects. + final AsyncContext asyncContext = super.startAsync(wrappedRequest, wrappedResponse); + return new FilterInvokingAsyncContext(asyncContext, filterInvoker, this, wrappedResponse); + } + + @Override + public AsyncContext getAsyncContext() { + final AsyncContext asyncContext = super.getAsyncContext(); + return new FilterInvokingAsyncContext(asyncContext, filterInvoker, this, servletResponse); + } + } + + // AsyncContext wrapper that is necessary for two reasons: + // 1) Run response filters when AsyncContext.complete() is called. + // 2) Eliminate paths where application code can get its hands on un-wrapped response object, circumventing + // running of response filters. + private static class FilterInvokingAsyncContext implements AsyncContext { + private final AsyncContext delegate; + private final OneTimeRunnable filterInvoker; + private final ServletRequest servletRequest; + private final ServletResponse servletResponse; + + public FilterInvokingAsyncContext( + AsyncContext delegate, + OneTimeRunnable filterInvoker, + ServletRequest servletRequest, + ServletResponse servletResponse) { + this.delegate = delegate; + this.filterInvoker = filterInvoker; + this.servletRequest = servletRequest; + this.servletResponse = servletResponse; + } + + @Override + public ServletRequest getRequest() { + return servletRequest; + } + + @Override + public ServletResponse getResponse() { + return servletResponse; + } + + @Override + public boolean hasOriginalRequestAndResponse() { + return delegate.hasOriginalRequestAndResponse(); + } + + @Override + public void dispatch() { + delegate.dispatch(); + } + + @Override + public void dispatch(String s) { + delegate.dispatch(s); + } + + @Override + public void dispatch(ServletContext servletContext, String s) { + delegate.dispatch(servletContext, s); + } + + @Override + public void complete() { + // Completing may commit the response, so this is the last chance to run response filters. + filterInvoker.runIfFirstInvocation(); + delegate.complete(); + } + + @Override + public void start(Runnable runnable) { + delegate.start(runnable); + } + + @Override + public void addListener(AsyncListener asyncListener) { + delegate.addListener(asyncListener); + } + + @Override + public void addListener(AsyncListener asyncListener, ServletRequest servletRequest, ServletResponse servletResponse) { + delegate.addListener(asyncListener, servletRequest, servletResponse); + } + + @Override + public <T extends AsyncListener> T createListener(Class<T> aClass) throws ServletException { + return delegate.createListener(aClass); + } + + @Override + public void setTimeout(long l) { + delegate.setTimeout(l); + } + + @Override + public long getTimeout() { + return delegate.getTimeout(); + } + } + + private static class FilterInvokingResponseWrapper extends HttpServletResponseWrapper { + private final OneTimeRunnable filterInvoker; + + public FilterInvokingResponseWrapper(HttpServletResponse response, OneTimeRunnable filterInvoker) { + super(response); + this.filterInvoker = filterInvoker; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + ServletOutputStream delegate = super.getOutputStream(); + return new FilterInvokingServletOutputStream(delegate, filterInvoker); + } + + @Override + public PrintWriter getWriter() throws IOException { + PrintWriter delegate = super.getWriter(); + return new FilterInvokingPrintWriter(delegate, filterInvoker); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java new file mode 100644 index 00000000000..e1f3581a1ca --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java @@ -0,0 +1,201 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.handler.OverloadException; + +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.websocket.server.WebSocketServerFactory; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.servlet.WebSocketServlet; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; +import java.util.logging.Level; + +import static com.yahoo.jdisc.http.server.jetty.ConnectorFactory.JDiscServerConnector; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +@WebServlet(asyncSupported = true, description = "Bridge between Servlet and JDisc APIs") +class JDiscHttpServlet extends WebSocketServlet { + public static final String ATTRIBUTE_NAME_ACCESS_LOG_ENTRY + = JDiscHttpServlet.class.getName() + "_access-log-entry"; + + private final static Logger log = Logger.getLogger(JDiscHttpServlet.class.getName()); + private final JDiscContext context; + + public JDiscHttpServlet(JDiscContext context) { + this.context = context; + } + + @Override + public void init() throws ServletException { + // The parent class of this loads the WebSocketServerFactory class using Class.forName() in the current thread's + // context class loader. To make sure that the class is available when running on OSGi, we configure it + // explicitly. This also has the required side-effect of generating the appropriate Import-Package statement in + // our OSGi bundle's manifest. + Thread.currentThread().setContextClassLoader(WebSocketServerFactory.class.getClassLoader()); + super.init(); + } + + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doPost(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doHead(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doPut(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doDelete(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doOptions(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doTrace(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + dispatchHttpRequest(request, response); + } + + @Override + public void configure(final WebSocketServletFactory factory) { + dispatchWebSocketRequest(factory); + } + + private static final Set<String> JETTY_UNSUPPORTED_METHODS = new HashSet<>(Arrays.asList( + "PATCH")); + + /** + * Override to set connector attribute before the request becomes an upgrade request in the web socket case. + * (After the upgrade, the HttpConnection is no longer available.) + */ + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.setAttribute(JDiscServerConnector.REQUEST_ATTRIBUTE, getConnector(request)); + + Metric.Context metricContext = getMetricContext(request); + context.metric.add(JettyHttpServer.Metrics.NUM_REQUESTS, 1, metricContext); + context.metric.add(JettyHttpServer.Metrics.JDISC_HTTP_REQUESTS, 1, metricContext); + context.metric.add(JettyHttpServer.Metrics.MANHATTAN_NUM_REQUESTS, 1, metricContext); + + if (JETTY_UNSUPPORTED_METHODS.contains(request.getMethod().toUpperCase())) { + dispatchHttpRequest(request, response); + } else { + super.service(request, response); + } + } + + static JDiscServerConnector getConnector(HttpServletRequest request) { + HttpConnection connection = (HttpConnection)request.getAttribute("org.eclipse.jetty.server.HttpConnection"); + return (JDiscServerConnector)connection.getConnector(); + } + + private void dispatchHttpRequest(final HttpServletRequest request, + final HttpServletResponse response) throws IOException { + final AccessLogEntry accessLogEntry = new AccessLogEntry(); + AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(request, accessLogEntry); + request.setAttribute(ATTRIBUTE_NAME_ACCESS_LOG_ENTRY, accessLogEntry); + try { + switch (request.getDispatcherType()) { + case REQUEST: + new HttpRequestDispatch(context, + accessLogEntry, + getMetricContext(request), + request, response).dispatch(); + break; + default: + if (log.isLoggable(Level.INFO)) { + log.info("Unexpected " + request.getDispatcherType() + "; " + + formatAttributes(request)); + } + break; + } + } catch (OverloadException e) { + // nop + } catch (RuntimeException e) { + throw new ExceptionWrapper(e); + } + } + + private void dispatchWebSocketRequest(final WebSocketServletFactory factory) { + try { + // any configuration of the websocket factory goes here + factory.setCreator(new WebSocketCreator() { + + @Override + public Object createWebSocket( + final ServletUpgradeRequest request, + final ServletUpgradeResponse response) { + + if (true) { + log.warning("WebSocket is currently not supported for JDisc RequestHandlers when running on Jetty."); + return null; + } + return new WebSocketRequestDispatch(context.container, context.janitor, context.metric, + getMetricContext(request.getHttpServletRequest())) + .dispatch(request, response); + } + }); + } catch (RuntimeException e) { + throw new ExceptionWrapper(e); + } + } + + private static Metric.Context getMetricContext(ServletRequest request) { + return JDiscServerConnector.fromRequest(request) + .getMetricContext(); + } + + private static String formatAttributes(final HttpServletRequest request) { + final StringBuilder out = new StringBuilder(); + out.append("attributes = {"); + for (Enumeration<String> names = request.getAttributeNames(); names.hasMoreElements(); ) { + String name = names.nextElement(); + out.append(" '").append(name).append("' = '").append(request.getAttribute(name)).append("'"); + if (names.hasMoreElements()) { + out.append(","); + } + } + out.append(" }"); + return out.toString(); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java new file mode 100644 index 00000000000..abebf109fc2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java @@ -0,0 +1,372 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.annotations.Beta; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.inject.Inject; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.server.FilterBindings; +import com.yahoo.jdisc.service.AbstractServerProvider; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ConnectorStatistics; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.RequestLog; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandlerContainer; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.RequestLogHandler; +import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +import javax.servlet.DispatcherType; +import java.nio.channels.FileChannel; +import java.nio.channels.ServerSocketChannel; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.ConnectorFactory.JDiscServerConnector; +import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +@Beta +public class JettyHttpServer extends AbstractServerProvider { + + public interface Metrics { + final String NAME_DIMENSION = "serverName"; + final String PORT_DIMENSION = "serverPort"; + + final String NUM_ACTIVE_REQUESTS = "serverNumActiveRequests"; + final String NUM_OPEN_CONNECTIONS = "serverNumOpenConnections"; + final String NUM_CONNECTIONS_OPEN_MAX = "serverConnectionsOpenMax"; + final String CONNECTION_DURATION_MAX = "serverConnectionDurationMax"; + final String CONNECTION_DURATION_MEAN = "serverConnectionDurationMean"; + final String CONNECTION_DURATION_STD_DEV = "serverConnectionDurationStdDev"; + + final String NUM_BYTES_RECEIVED = "serverBytesReceived"; + final String NUM_BYTES_SENT = "serverBytesSent"; + final String MANHATTAN_NUM_BYTES_RECEIVED = "http.in.bytes"; + final String MANHATTAN_NUM_BYTES_SENT = "http.out.bytes"; + + final String NUM_CONNECTIONS = "serverNumConnections"; + final String NUM_CONNECTIONS_IDLE = "serverNumConnectionsIdle"; + final String NUM_UNEXPECTED_DISCONNECTS = "serverNumUnexpectedDisconnects"; + + /* For historical reasons, these are all aliases for the same metric. 'jdisc.http' should ideally be the only one. */ + final String JDISC_HTTP_REQUESTS = "jdisc.http.requests"; + final String NUM_REQUESTS = "serverNumRequests"; + final String MANHATTAN_NUM_REQUESTS = "http.requests"; + + final String NUM_SUCCESSFUL_RESPONSES = "serverNumSuccessfulResponses"; + final String NUM_FAILED_RESPONSES = "serverNumFailedResponses"; + final String NUM_SUCCESSFUL_WRITES = "serverNumSuccessfulResponseWrites"; + final String NUM_FAILED_WRITES = "serverNumFailedResponseWrites"; + + final String NETWORK_LATENCY = "serverNetworkLatency"; + final String TOTAL_SUCCESSFUL_LATENCY = "serverTotalSuccessfulResponseLatency"; + final String MANHATTAN_TOTAL_SUCCESSFUL_LATENCY = "http.latency"; + final String TOTAL_FAILED_LATENCY = "serverTotalFailedResponseLatency"; + final String TIME_TO_FIRST_BYTE = "serverTimeToFirstByte"; + final String MANHATTAN_TIME_TO_FIRST_BYTE = "http.out.firstbytetime"; + + final String RESPONSES_1XX = "http.status.1xx"; + final String RESPONSES_2XX = "http.status.2xx"; + final String RESPONSES_3XX = "http.status.3xx"; + final String RESPONSES_4XX = "http.status.4xx"; + final String RESPONSES_5XX = "http.status.5xx"; + + final String STARTED_MILLIS = "serverStartedMillis"; + final String MANHATTAN_STARTED_MILLIS = "proc.uptime"; + } + + private final static Logger log = Logger.getLogger(JettyHttpServer.class.getName()); + private final long timeStarted = System.currentTimeMillis(); + private final ExecutorService janitor; + private final ScheduledExecutorService metricReporterExecutor; + private final Metric metric; + private final Server server; + + @Inject + public JettyHttpServer( + final CurrentContainer container, + final Metric metric, + final ServerConfig serverConfig, + final ServletPathsConfig servletPathsConfig, + final ThreadFactory threadFactory, + final FilterBindings filterBindings, + final ComponentRegistry<ConnectorFactory> connectorFactories, + final ComponentRegistry<ServletHolder> servletHolders, + final OsgiFramework osgiFramework, + final FilterInvoker filterInvoker, + final AccessLog accessLog) { + super(container); + if (connectorFactories.allComponents().isEmpty()) { + throw new IllegalArgumentException("No connectors configured."); + } + this.metric = metric; + + server = new Server(); + ((QueuedThreadPool)server.getThreadPool()).setMaxThreads(serverConfig.maxWorkerThreads()); + + Map<Path, FileChannel> keyStoreChannels = getKeyStoreFileChannels(osgiFramework.bundleContext()); + + for (ConnectorFactory connectorFactory : connectorFactories.allComponents()) { + ServerSocketChannel preBoundChannel = getChannelFromServiceLayer(connectorFactory.getConnectorConfig().listenPort(), osgiFramework.bundleContext()); + server.addConnector(connectorFactory.createConnector(metric, server, preBoundChannel, keyStoreChannels)); + } + + janitor = newJanitor(threadFactory); + + JDiscContext jDiscContext = new JDiscContext( + filterBindings.getRequestFilters().activate(), + filterBindings.getResponseFilters().activate(), + container, + janitor, + metric, + serverConfig); + + ServletHolder jdiscServlet = new ServletHolder(new JDiscHttpServlet(jDiscContext)); + FilterHolder jDiscFilterInvokerFilter = new FilterHolder(new JDiscFilterInvokerFilter(jDiscContext, filterInvoker)); + + final RequestLog requestLog = new AccessLogRequestLog(accessLog); + + server.setHandler( + getHandlerCollection( + serverConfig, + servletPathsConfig, + jdiscServlet, + servletHolders, + jDiscFilterInvokerFilter, + requestLog)); + + final int numMetricReporterThreads = 1; + metricReporterExecutor = Executors.newScheduledThreadPool( + numMetricReporterThreads, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat(JettyHttpServer.class.getName() + "-MetricReporter-%d") + .setThreadFactory(threadFactory) + .build() + ); + metricReporterExecutor.scheduleAtFixedRate(new MetricTask(), 0, 2, TimeUnit.SECONDS); + } + + private HandlerCollection getHandlerCollection( + ServerConfig serverConfig, + ServletPathsConfig servletPathsConfig, + ServletHolder jdiscServlet, + ComponentRegistry<ServletHolder> servletHolders, + FilterHolder jDiscFilterInvokerFilter, + RequestLog requestLog) { + + ServletContextHandler servletContextHandler = createServletContextHandler(); + + servletHolders.allComponentsById().forEach((id, servlet) -> { + String path = getServletPath(servletPathsConfig, id); + servletContextHandler.addServlet(servlet, path); + servletContextHandler.addFilter(jDiscFilterInvokerFilter, path, EnumSet.allOf(DispatcherType.class)); + }); + + servletContextHandler.addServlet(jdiscServlet, "/*"); + + final GzipHandler gzipHandler = newGzipHandler(serverConfig); + gzipHandler.setHandler(servletContextHandler); + + final StatisticsHandler statisticsHandler = newStatisticsHandler(); + statisticsHandler.setHandler(gzipHandler); + + final RequestLogHandler requestLogHandler = new RequestLogHandler(); + requestLogHandler.setRequestLog(requestLog); + + HandlerCollection handlerCollection = new HandlerCollection(); + handlerCollection.setHandlers(new Handler[]{statisticsHandler, requestLogHandler}); + return handlerCollection; + } + + private static String getServletPath(ServletPathsConfig servletPathsConfig, ComponentId id) { + return "/" + servletPathsConfig.servlets(id.stringValue()).path(); + } + + // Ugly trick to get generic type literal. + @SuppressWarnings("unchecked") + private static final Class<Map<?, ?>> mapClass = (Class<Map<?, ?>>) (Object) Map.class; + + private Map<Path, FileChannel> getKeyStoreFileChannels(BundleContext bundleContext) { + try { + Collection<ServiceReference<Map<?, ?>>> serviceReferences = bundleContext.getServiceReferences(mapClass, + "(role=com.yahoo.container.standalone.StandaloneContainerActivator.KeyStoreFileChannels)"); + + if (serviceReferences == null || serviceReferences.isEmpty()) + return Collections.emptyMap(); + + if (serviceReferences.size() != 1) + throw new IllegalStateException("Multiple KeyStoreFileChannels registered"); + + return getKeyStoreFileChannels(bundleContext, serviceReferences.iterator().next()); + } catch (InvalidSyntaxException e) { + throw throwUnchecked(e); + } + } + + @SuppressWarnings("unchecked") + private Map<Path, FileChannel> getKeyStoreFileChannels(BundleContext bundleContext, ServiceReference<Map<?, ?>> keyStoreFileChannelReference) { + Map<?, ?> fileChannelMap = bundleContext.getService(keyStoreFileChannelReference); + try { + if (fileChannelMap == null) + return Collections.emptyMap(); + + Map<Path, FileChannel> result = (Map<Path, FileChannel>) fileChannelMap; + log.fine("Using file channel for " + result.keySet()); + return result; + } finally { + //if we change this to be anything other than a simple map, we should hold the reference as long as the object is in use. + bundleContext.ungetService(keyStoreFileChannelReference); + } + } + + private ServletContextHandler createServletContextHandler() { + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + servletContextHandler.setContextPath("/"); + return servletContextHandler; + } + + private ServerSocketChannel getChannelFromServiceLayer(int listenPort, BundleContext bundleContext) { + log.log(Level.FINE, "Retrieving channel for port " + listenPort + " from " + bundleContext.getClass().getName()); + Collection<ServiceReference<ServerSocketChannel>> refs; + final String filter = "(port=" + listenPort + ")"; + try { + refs = bundleContext.getServiceReferences(ServerSocketChannel.class, filter); + } catch (InvalidSyntaxException e) { + throw new IllegalStateException("OSGi framework rejected filter " + filter, e); + } + if (refs.isEmpty()) { + return null; + } + if (refs.size() != 1) { + throw new IllegalStateException("Got more than one service reference for " + ServerSocketChannel.class + " port " + listenPort + "."); + } + ServiceReference<ServerSocketChannel> ref = refs.iterator().next(); + return bundleContext.getService(ref); + } + + private static ExecutorService newJanitor(final ThreadFactory factory) { + final int threadPoolSize = Runtime.getRuntime().availableProcessors(); + log.info("Creating janitor executor with " + threadPoolSize + " threads"); + return Executors.newFixedThreadPool( + threadPoolSize, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat(JettyHttpServer.class.getName() + "-Janitor-%d") + .setThreadFactory(factory) + .build() + ); + } + + @Override + public void start() { + try { + server.start(); + } catch (final Exception e) { + throw new RuntimeException("Failed to start server.", e); + } + } + + @Override + public void close() { + try { + server.stop(); + } catch (final Exception e) { + log.log(Level.SEVERE, "Server shutdown threw an unexpected exception.", e); + } + + metricReporterExecutor.shutdown(); + janitor.shutdown(); + } + + public int getListenPort() { + return ((ServerConnector)server.getConnectors()[0]).getLocalPort(); + } + + private class MetricTask implements Runnable { + @Override + public void run() { + StatisticsHandler statisticsHandler = ((AbstractHandlerContainer)server.getHandler()) + .getChildHandlerByClass(StatisticsHandler.class); + if (statisticsHandler == null) + return; + + setServerMetrics(statisticsHandler); + + for (Connector connector : server.getConnectors()) { + setConnectorMetrics((JDiscServerConnector)connector); + } + } + + } + + private void setServerMetrics(StatisticsHandler statistics) { + long timeSinceStarted = System.currentTimeMillis() - timeStarted; + metric.set(Metrics.STARTED_MILLIS, timeSinceStarted, null); + metric.set(Metrics.MANHATTAN_STARTED_MILLIS, timeSinceStarted, null); + + metric.add(Metrics.RESPONSES_1XX, statistics.getResponses1xx(), null); + metric.add(Metrics.RESPONSES_2XX, statistics.getResponses2xx(), null); + metric.add(Metrics.RESPONSES_3XX, statistics.getResponses3xx(), null); + metric.add(Metrics.RESPONSES_4XX, statistics.getResponses4xx(), null); + metric.add(Metrics.RESPONSES_5XX, statistics.getResponses5xx(), null); + + // Reset to only add the diff for count metrics. + // (The alternative to reset would be to preserve the previous value, and only add the diff.) + statistics.statsReset(); + } + + private void setConnectorMetrics(JDiscServerConnector connector) { + ConnectorStatistics statistics = connector.getStatistics(); + metric.set(Metrics.NUM_CONNECTIONS, statistics.getConnections(), connector.getMetricContext()); + metric.set(Metrics.NUM_OPEN_CONNECTIONS, statistics.getConnectionsOpen(), connector.getMetricContext()); + metric.set(Metrics.NUM_CONNECTIONS_OPEN_MAX, statistics.getConnectionsOpenMax(), connector.getMetricContext()); + metric.set(Metrics.CONNECTION_DURATION_MAX, statistics.getConnectionDurationMax(), connector.getMetricContext()); + metric.set(Metrics.CONNECTION_DURATION_MEAN, statistics.getConnectionDurationMean(), connector.getMetricContext()); + metric.set(Metrics.CONNECTION_DURATION_STD_DEV, statistics.getConnectionDurationStdDev(), connector.getMetricContext()); + } + + private StatisticsHandler newStatisticsHandler() { + StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.statsReset(); + return statisticsHandler; + } + + private GzipHandler newGzipHandler(ServerConfig serverConfig) { + final GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setCompressionLevel(serverConfig.responseCompressionLevel()); + gzipHandler.setCheckGzExists(false); + gzipHandler.setIncludedMethods("GET", "POST"); + return gzipHandler; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java new file mode 100644 index 00000000000..518c9f92ea8 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Metric.Context; + +import com.yahoo.jdisc.http.server.jetty.JettyHttpServer.Metrics; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * Responsible for metric reporting for JDisc http and web socket request handler support. + * @author tonytv + */ +public class MetricReporter { + private final Metric metric; + private final @Nullable Context context; + + private final long requestStartTime; + + //TODO: rename + private final AtomicBoolean firstSetOfTimeToFirstByte = new AtomicBoolean(true); + + + public MetricReporter(Metric metric, @Nullable Context context, long requestStartTime) { + this.metric = metric; + this.context = context; + this.requestStartTime = requestStartTime; + } + + public void successfulWrite(int numBytes) { + setTimeToFirstByteFirstTime(); + + metric.add(Metrics.NUM_SUCCESSFUL_WRITES, 1, context); + metric.set(Metrics.NUM_BYTES_SENT, numBytes, context); + metric.set(Metrics.MANHATTAN_NUM_BYTES_SENT, numBytes, context); + } + + private void setTimeToFirstByteFirstTime() { + boolean isFirstWrite = firstSetOfTimeToFirstByte.getAndSet(false); + if (isFirstWrite) { + long timeToFirstByte = getRequestLatency(); + metric.set(Metrics.TIME_TO_FIRST_BYTE, timeToFirstByte, context); + metric.set(Metrics.MANHATTAN_TIME_TO_FIRST_BYTE, timeToFirstByte, context); + } + } + + public void failedWrite() { + metric.add(Metrics.NUM_FAILED_WRITES, 1, context); + } + + public void successfulResponse() { + setTimeToFirstByteFirstTime(); + + long requestLatency = getRequestLatency(); + + metric.set(Metrics.TOTAL_SUCCESSFUL_LATENCY, requestLatency, context); + metric.set(Metrics.MANHATTAN_TOTAL_SUCCESSFUL_LATENCY, requestLatency, context); + + metric.add(Metrics.NUM_SUCCESSFUL_RESPONSES, 1, context); + } + + public void failedResponse() { + setTimeToFirstByteFirstTime(); + + metric.set(Metrics.TOTAL_FAILED_LATENCY, getRequestLatency(), context); + metric.add(Metrics.NUM_FAILED_RESPONSES, 1, context); + } + + public void successfulRead(int bytes_received) { + metric.set(JettyHttpServer.Metrics.NUM_BYTES_RECEIVED, bytes_received, context); + metric.set(JettyHttpServer.Metrics.MANHATTAN_NUM_BYTES_RECEIVED, bytes_received, context); + } + + private long getRequestLatency() { + return System.currentTimeMillis() - requestStartTime; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java new file mode 100644 index 00000000000..1d6d7a55b69 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author tonytv + */ +public class OneTimeRunnable { + private final Runnable runnable; + private final AtomicBoolean hasRun = new AtomicBoolean(false); + + public OneTimeRunnable(Runnable runnable) { + this.runnable = runnable; + } + + public void runIfFirstInvocation() { + boolean previous = hasRun.getAndSet(true); + if (!previous) { + runnable.run(); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java new file mode 100644 index 00000000000..d8012880694 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java @@ -0,0 +1,256 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.NullContent; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class wraps a request handler and does reference counting on the request for every object that depends on the + * request, such as the response handler, content channels and completion handlers. This ensures that requests (and + * hence the current container) will be referenced until the end of the request handling - even with async handling in + * non-framework threads - without requiring the application to handle this tedious work. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +class ReferenceCountingRequestHandler implements RequestHandler { + + private static final Logger log = Logger.getLogger(ReferenceCountingRequestHandler.class.getName()); + + final RequestHandler delegate; + + ReferenceCountingRequestHandler(RequestHandler delegate) { + Objects.requireNonNull(delegate, "delegate"); + this.delegate = delegate; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + try (final ResourceReference requestReference = request.refer()) { + ContentChannel contentChannel; + final ReferenceCountingResponseHandler referenceCountingResponseHandler + = new ReferenceCountingResponseHandler(request, new NullContentResponseHandler(responseHandler)); + try { + contentChannel = delegate.handleRequest(request, referenceCountingResponseHandler); + Objects.requireNonNull(contentChannel, "contentChannel"); + } catch (Throwable t) { + try { + // The response handler might never be invoked, due to the exception thrown from handleRequest(). + referenceCountingResponseHandler.unrefer(); + } catch (Throwable thrownFromUnrefer) { + log.log(Level.WARNING, "Unexpected problem", thrownFromUnrefer); + } + throw t; + } + return new ReferenceCountingContentChannel(request, contentChannel); + } + } + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + delegate.handleTimeout(request, new NullContentResponseHandler(responseHandler)); + } + + @Override + public ResourceReference refer() { + return delegate.refer(); + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public String toString() { + return delegate.toString(); + } + + private static class ReferenceCountingResponseHandler implements ResponseHandler { + + final SharedResource request; + final ResourceReference requestReference; + final ResponseHandler delegate; + final AtomicBoolean closed = new AtomicBoolean(false); + + ReferenceCountingResponseHandler(SharedResource request, ResponseHandler delegate) { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(delegate, "delegate"); + this.request = request; + this.delegate = delegate; + this.requestReference = request.refer(); + } + + @Override + public ContentChannel handleResponse(Response response) { + if (closed.getAndSet(true)) { + throw new IllegalStateException(delegate + " is already called."); + } + try (final ResourceReference ref = requestReference) { + ContentChannel contentChannel = delegate.handleResponse(response); + Objects.requireNonNull(contentChannel, "contentChannel"); + return new ReferenceCountingContentChannel(request, contentChannel); + } + } + + @Override + public String toString() { + return delegate.toString(); + } + + /** + * Close the reference that is normally closed by {@link #handleResponse(Response)}. + * + * This is to be used in error situations, where handleResponse() may not be invoked. + */ + public void unrefer() { + if (closed.getAndSet(true)) { + // This simply means that handleResponse() has been run, in which case we are + // guaranteed that the reference is closed. + return; + } + requestReference.close(); + } + } + + private static class ReferenceCountingContentChannel implements ContentChannel { + + final SharedResource request; + final ResourceReference requestReference; + final ContentChannel delegate; + + ReferenceCountingContentChannel(SharedResource request, ContentChannel delegate) { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(delegate, "delegate"); + this.request = request; + this.delegate = delegate; + this.requestReference = request.refer(); + } + + @Override + public void write(ByteBuffer buf, CompletionHandler completionHandler) { + final CompletionHandler referenceCountingCompletionHandler + = new ReferenceCountingCompletionHandler(request, completionHandler); + try { + delegate.write(buf, referenceCountingCompletionHandler); + } catch (Throwable t) { + try { + referenceCountingCompletionHandler.failed(t); + } catch (AlreadyCompletedException ignored) { + } catch (Throwable failFailure) { + log.log(Level.WARNING, "Failure during call to CompletionHandler.failed()", failFailure); + } + throw t; + } + } + + @Override + public void close(CompletionHandler completionHandler) { + final CompletionHandler referenceCountingCompletionHandler + = new ReferenceCountingCompletionHandler(request, completionHandler); + try (final ResourceReference ref = requestReference) { + delegate.close(referenceCountingCompletionHandler); + } catch (Throwable t) { + try { + referenceCountingCompletionHandler.failed(t); + } catch (AlreadyCompletedException ignored) { + } catch (Throwable failFailure) { + log.log(Level.WARNING, "Failure during call to CompletionHandler.failed()", failFailure); + } + throw t; + } + } + + @Override + public String toString() { + return delegate.toString(); + } + } + + private static class AlreadyCompletedException extends IllegalStateException { + public AlreadyCompletedException(final CompletionHandler completionHandler) { + super(completionHandler + " is already called."); + } + } + + private static class ReferenceCountingCompletionHandler implements CompletionHandler { + + final ResourceReference requestReference; + final CompletionHandler delegate; + final AtomicBoolean closed = new AtomicBoolean(false); + + public ReferenceCountingCompletionHandler(SharedResource request, CompletionHandler delegate) { + this.delegate = delegate; + this.requestReference = request.refer(); + } + + @Override + public void completed() { + if (closed.getAndSet(true)) { + throw new AlreadyCompletedException(delegate); + } + try { + if (delegate != null) { + delegate.completed(); + } + } finally { + requestReference.close(); + } + } + + @Override + public void failed(Throwable t) { + if (closed.getAndSet(true)) { + throw new AlreadyCompletedException(delegate); + } + try (final ResourceReference ref = requestReference) { + if (delegate != null) { + delegate.failed(t); + } else { + log.log(Level.WARNING, "Uncaught completion failure.", t); + } + } + } + + @Override + public String toString() { + return String.valueOf(delegate); + } + } + + private static class NullContentResponseHandler implements ResponseHandler { + + final ResponseHandler delegate; + + NullContentResponseHandler(ResponseHandler delegate) { + Objects.requireNonNull(delegate, "delegate"); + this.delegate = delegate; + } + + @Override + public ContentChannel handleResponse(Response response) { + ContentChannel contentChannel = delegate.handleResponse(response); + if (contentChannel == null) { + contentChannel = NullContent.INSTANCE; + } + return contentChannel; + } + + @Override + public String toString() { + return delegate.toString(); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java new file mode 100644 index 00000000000..cbcbd278bf8 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +/** + * This exception may be thrown from a request handler to fail a request with a given response code and message. + * It is given some special treatment in {@link ServletResponseController}. + * + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +class RequestException extends RuntimeException { + + private final int responseStatus; + + /** + * @param responseStatus the response code to use for the http response + * @param message exception message + * @param cause chained throwable + */ + public RequestException(final int responseStatus, final String message, final Throwable cause) { + super(message, cause); + this.responseStatus = responseStatus; + } + + /** + * @param responseStatus the response code to use for the http response + * @param message exception message + */ + public RequestException(final int responseStatus, final String message) { + super(message); + this.responseStatus = responseStatus; + } + + /** + * Returns the response code to use for the http response. + */ + public int getResponseStatus() { + return responseStatus; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java new file mode 100644 index 00000000000..58cdd7a331e --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.handler.CompletionHandler; + +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author tonytv + * @author simon + */ +class ResponseContentPart { + private static final Logger log = Logger.getLogger(ResponseContentPart.class.getName()); + + final ByteBuffer buf; + final CompletionHandler handler; + + ResponseContentPart(final ByteBuffer buf, final CompletionHandler handler) { + this.buf = (buf != null) ? buf : ByteBuffer.allocate(0); + this.handler = (handler != null) ? handler: DEFAULT_COMPLETION_HANDLER; + } + + private static final CompletionHandler DEFAULT_COMPLETION_HANDLER = new CompletionHandler() { + @Override + public void completed() { + log.log(Level.FINE, "DefaultCompletionHandler: Operation completed"); + } + + @Override + public void failed(Throwable t) { + log.log(Level.FINE, "DefaultCompletionHandler: Operation failed", t); + } + }; +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java new file mode 100644 index 00000000000..271805765c2 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java @@ -0,0 +1,286 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.handler.CompletionHandler; + +import javax.annotation.concurrent.GuardedBy; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author tonytv + */ +public class ServletOutputStreamWriter { + /** Rules: + * 1) Don't modify the output stream without isReady returning true (write/flush/close). + * Multiple modification calls without interleaving isReady calls are not allowed. + * 2) If isReady returned false, no other calls should be made until the write listener is invoked. + * 3) If the write listener sees isReady == false, it must not do any modifications before its next invocation. + */ + + + private enum State { + NOT_STARTED, + WAITING_FOR_WRITE_POSSIBLE_CALLBACK, + WAITING_FOR_BUFFER, + WRITING_BUFFERS, + FINISHED_OR_ERROR + } + + private static final Logger log = Logger.getLogger(ServletOutputStreamWriter.class.getName()); + + private static final ByteBuffer CLOSE_STREAM_BUFFER = ByteBuffer.allocate(0); + + private final Object monitor = new Object(); + + @GuardedBy("monitor") + private State state = State.NOT_STARTED; + + @GuardedBy("state") + private final ServletOutputStream outputStream; + private final Executor executor; + + @GuardedBy("monitor") + private final Deque<ResponseContentPart> responseContentQueue = new ArrayDeque<>(); + + private final MetricReporter metricReporter; + + /** + * When this future completes there will be no more calls against the servlet output stream or servlet response. + * The framework is still allowed to invoke us though. + * + * The future might complete in the servlet framework thread, user thread or executor thread. + */ + final CompletableFuture<Void> finishedFuture = new CompletableFuture<>(); + + + public ServletOutputStreamWriter(ServletOutputStream outputStream, Executor executor, MetricReporter metricReporter) { + this.outputStream = outputStream; + this.executor = executor; + this.metricReporter = metricReporter; + } + + public void setSendingError() { + synchronized (monitor) { + assertStateIs(state, State.NOT_STARTED); + state = State.FINISHED_OR_ERROR; + } + } + + public void writeBuffer(ByteBuffer buf, CompletionHandler handler) { + boolean thisThreadShouldWrite = false; + + synchronized (monitor) { + if (state == State.FINISHED_OR_ERROR) { + if (handler != null) { + executor.execute(() -> handler.failed(new IllegalStateException("ContentChannel already closed."))); + } + return; + } + + responseContentQueue.addLast(new ResponseContentPart(buf, handler)); + switch (state) { + case NOT_STARTED: + state = State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK; + outputStream.setWriteListener(writeListener); + break; + case WAITING_FOR_WRITE_POSSIBLE_CALLBACK: + case WRITING_BUFFERS: + break; + case WAITING_FOR_BUFFER: + thisThreadShouldWrite = true; + state = State.WRITING_BUFFERS; + break; + default: + throw new IllegalStateException("Invalid state " + state); + } + } + + if (thisThreadShouldWrite) { + writeBuffersInQueueToOutputStream(); + } + } + + public void close(CompletionHandler handler) { + writeBuffer(CLOSE_STREAM_BUFFER, handler); + } + + private void writeBuffersInQueueToOutputStream() { + boolean lastOperationWasFlush = false; + + while (true) { + ResponseContentPart contentPart; + + synchronized (monitor) { + if (state == State.FINISHED_OR_ERROR) { + return; + } + + assertStateIs(state, State.WRITING_BUFFERS); + + if (!outputStream.isReady()) { + state = State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK; + return; + } + + contentPart = responseContentQueue.pollFirst(); + + if (contentPart == null && lastOperationWasFlush) { + state = State.WAITING_FOR_BUFFER; + return; + } + } + + try { + boolean isFlush = contentPart == null; + if (isFlush) { + outputStream.flush(); + lastOperationWasFlush = true; + continue; + } + lastOperationWasFlush = false; + + if (contentPart.buf == CLOSE_STREAM_BUFFER) { + closeOutputStream(contentPart.handler); + setFinished(Optional.empty()); + } else { + writeBufferToOutputStream(contentPart); + } + } catch (Throwable e) { + setFinished(Optional.of(e)); + } + } + } + + private void setFinished(Optional<Throwable> e) { + synchronized (monitor) { + state = State.FINISHED_OR_ERROR; + if (!responseContentQueue.isEmpty()) { + failAllParts_holdingLock(e.orElse(new IllegalStateException("ContentChannel closed."))); + } + } + + assert !Thread.holdsLock(monitor); + if (e.isPresent()) { + finishedFuture.completeExceptionally(e.get()); + } else { + finishedFuture.complete(null); + } + } + + private void failAllParts_holdingLock(Throwable e) { + assert Thread.holdsLock(monitor); + + ArrayList<ResponseContentPart> failedParts = new ArrayList<>(responseContentQueue); + responseContentQueue.clear(); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + RuntimeException failReason = new RuntimeException("Failing due to earlier ServletOutputStream write failure", e); + + Consumer<ResponseContentPart> failCompletionHandler = responseContentPart -> + runCompletionHandler_logOnExceptions( + () -> responseContentPart.handler.failed(failReason)); + + executor.execute( + () -> failedParts.forEach(failCompletionHandler)); + } + + private void closeOutputStream(CompletionHandler handler) throws Exception { + callCompletionHandlerWhenDone(handler, () -> { + outputStream.close(); + return null; + }); + } + + private void writeBufferToOutputStream(ResponseContentPart contentPart) throws Throwable { + callCompletionHandlerWhenDone(contentPart.handler, () -> { + ByteBuffer buffer = contentPart.buf; + final int bytesToSend = buffer.remaining(); + try { + if (buffer.hasArray()) { + outputStream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining()); + } else { + final byte[] array = new byte[buffer.remaining()]; + buffer.get(array); + outputStream.write(array); + } + metricReporter.successfulWrite(bytesToSend); + } catch (Throwable throwable) { + metricReporter.failedWrite(); + throw throwable; + } + + return null; + }); + } + + //Using Callable<Void> instead of Runnable since Callable supports throwing exceptions. + private void callCompletionHandlerWhenDone(CompletionHandler handler, Callable<Void> callable) throws Exception { + try { + callable.call(); + } catch (Throwable e) { + assert !Thread.holdsLock(monitor); + runCompletionHandler_logOnExceptions( + () -> handler.failed(e)); + throw e; + } + + assert !Thread.holdsLock(monitor); + handler.completed(); //Might throw an exception, handling in the enclosing scope. + } + + private void runCompletionHandler_logOnExceptions(Runnable runnable) { + assert !Thread.holdsLock(monitor); + try { + runnable.run(); + } catch (Throwable e) { + log.log(Level.WARNING, "Unexpected exception from CompletionHandler.", e); + } + } + + private void assertStateIs(State currentState, State expectedState) { + if (currentState != expectedState) { + AssertionError error = new AssertionError("Expected state " + expectedState + ", got state " + currentState); + log.log(Level.WARNING, "Assertion failed.", error); + throw error; + } + } + + public void fail(Throwable t) { + setFinished(Optional.of(t)); + } + + private final WriteListener writeListener = new WriteListener() { + @Override + public void onWritePossible() throws IOException { + synchronized (monitor) { + if (state == State.FINISHED_OR_ERROR) { + return; + } + + assertStateIs(state, State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK); + state = State.WRITING_BUFFERS; + } + + writeBuffersInQueueToOutputStream(); + } + + @Override + public void onError(Throwable t) { + setFinished(Optional.of(t)); + } + }; + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java new file mode 100644 index 00000000000..5bea01bd104 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java @@ -0,0 +1,266 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; + +import javax.annotation.concurrent.GuardedBy; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Finished when either + * 1) There was an error + * 2) There is no more data AND the number of pending completion handler invocations is 0 + * + * Stops reading when a failure has happened. + * + * The reason for not waiting for pending completions in error situations + * is that if the error is reported through the finishedFuture, + * error reporting might be async. + * Since we have tests that first reports errors and then closes the response content, + * it's important that errors are delivered synchronously. + */ +class ServletRequestReader implements ReadListener { + private enum State { + READING, ALL_DATA_READ, REQUEST_CONTENT_CLOSED + } + + private static final Logger log = Logger.getLogger(ServletRequestReader.class.getName()); + + private static final int MIN_BUFFER_SIZE_BYTES = 1024; + + private final Object monitor = new Object(); + + private final ServletInputStream servletInputStream; + private final ContentChannel requestContentChannel; + + private final Executor executor; + private final MetricReporter metricReporter; + + /** + * Rules: + * 1. If state != State.READING, then numberOfOutstandingUserCalls must not increase + * 2. The _first time_ (finishedFuture is completed OR all data is read) AND numberOfOutstandingUserCalls == 0, + * the request content channel should be closed + * 3. finishedFuture must not be completed when holding the monitor + * 4. completing finishedFuture with an exception must be done synchronously + * to prioritize failures being transported to the response. + * 5. All completion handlers (both for write and complete) must not be + * called from a user (request handler) owned thread + * (i.e. when being called from user code, don't call back into user code.) + */ + @GuardedBy("monitor") + private State state = State.READING; + + /** + * Number of calls that we're waiting for from user code. + * There are two classes of such calls: + * 1) calls to requestContentChannel.write that we're waiting for to complete + * 2) completion handlers given to requestContentChannel.write that the user must call. + * + * As long as we're waiting for such calls, we're not allowed to: + * - close the request content channel (currently only required by tests) + * - complete the finished future non-exceptionally, + * since then we would not be able to report writeCompletionHandler.failed(exception) calls + */ + @GuardedBy("monitor") + private int numberOfOutstandingUserCalls = 0; + + /** + * When this future completes there will be no more calls against the servlet input stream. + * The framework is still allowed to invoke us though. + * + * The future might complete in the servlet framework thread, user thread or executor thread. + * + * All completions of finishedFuture, except those done when closing the request content channel, + * must be followed by calls to either onAllDataRead or decreasePendingAndCloseRequestContentChannelConditionally. + * Those two functions will ensure that the request content channel is closed at the right time. + * If calls to those methods does not close the request content channel immediately, + * there is some outstanding completion callback that will later come in and complete the request. + */ + final CompletableFuture<Void> finishedFuture = new CompletableFuture<>(); + + public ServletRequestReader( + ServletInputStream servletInputStream, + ContentChannel requestContentChannel, + Executor executor, + MetricReporter metricReporter) { + + Preconditions.checkNotNull(servletInputStream); + Preconditions.checkNotNull(requestContentChannel); + Preconditions.checkNotNull(executor); + Preconditions.checkNotNull(metricReporter); + + this.servletInputStream = servletInputStream; + this.requestContentChannel = requestContentChannel; + this.executor = executor; + this.metricReporter = metricReporter; + } + + @Override + public void onDataAvailable() throws IOException { + while (servletInputStream.isReady()) { + final int estimatedNumBytesAvailable = servletInputStream.available(); + final int bufferSizeBytes = Math.max(estimatedNumBytesAvailable, MIN_BUFFER_SIZE_BYTES); + final byte[] buffer = new byte[bufferSizeBytes]; + final int numBytesRead = servletInputStream.read(buffer); + if (numBytesRead < 0) { + // End of stream; there should be no more data available, ever. + return; + } + writeRequestContent(ByteBuffer.wrap(buffer, 0, numBytesRead)); + } + } + + private void writeRequestContent(final ByteBuffer buf) { + synchronized (monitor) { + if (state != State.READING) { + //We have a failure, so no point in giving the buffer to the user. + assert finishedFuture.isCompletedExceptionally(); + return; + } + //wait for both + // - requestContentChannel.write to finish + // - the write completion handler to be called + numberOfOutstandingUserCalls += 2; + } + try { + requestContentChannel.write(buf, writeCompletionHandler); + + int bytesReceived = buf.remaining(); + metricReporter.successfulRead(bytesReceived); + } catch (final Throwable t) { + finishedFuture.completeExceptionally(t); + } finally { + //decrease due to this method completing. + decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally(); + } + } + + private void decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally() { + final boolean shouldCloseRequestContentChannel; + + synchronized (monitor) { + assertStateNotEquals(state, State.REQUEST_CONTENT_CLOSED); + + + numberOfOutstandingUserCalls -= 1; + + shouldCloseRequestContentChannel = numberOfOutstandingUserCalls == 0 && + (finishedFuture.isDone() || state == State.ALL_DATA_READ); + + if (shouldCloseRequestContentChannel) { + state = State.REQUEST_CONTENT_CLOSED; + } + } + + if (shouldCloseRequestContentChannel) { + executor.execute(this::closeCompletionHandler_noThrow); + } + } + + private void assertStateNotEquals(State state, State notExpectedState) { + if (state == notExpectedState) { + AssertionError e = new AssertionError("State should not be " + notExpectedState); + log.log(Level.WARNING, + "Assertion failed. " + + "numberOfOutstandingUserCalls = " + numberOfOutstandingUserCalls + + ", isDone = " + finishedFuture.isDone(), + e); + throw e; + } + } + + @Override + public void onAllDataRead() { + doneReading(); + } + + private void doneReading() { + final boolean shouldCloseRequestContentChannel; + + synchronized (monitor) { + if (state != State.READING) { + return; + } + + state = State.ALL_DATA_READ; + + shouldCloseRequestContentChannel = numberOfOutstandingUserCalls == 0; + if (shouldCloseRequestContentChannel) { + state = State.REQUEST_CONTENT_CLOSED; + } + } + + if (shouldCloseRequestContentChannel) { + closeCompletionHandler_noThrow(); + } + } + + private void closeCompletionHandler_noThrow() { + //Cannot complete finishedFuture directly in completed(), as any exceptions after this fact will be ignored. + // E.g. + // close(CompletionHandler completionHandler) { + // completionHandler.completed(); + // throw new RuntimeException + // } + + CompletableFuture<Void> completedCalledFuture = new CompletableFuture<>(); + + CompletionHandler closeCompletionHandler = new CompletionHandler() { + @Override + public void completed() { + completedCalledFuture.complete(null); + } + + @Override + public void failed(final Throwable t) { + finishedFuture.completeExceptionally(t); + } + }; + + try { + requestContentChannel.close(closeCompletionHandler); + //if close did not cause an exception, + // is it safe to pipe the result of the completionHandlerInvokedFuture into finishedFuture + completedCalledFuture.whenComplete(this::setFinishedFuture); + } catch (final Throwable t) { + finishedFuture.completeExceptionally(t); + } + } + + private void setFinishedFuture(Void result, Throwable throwable) { + if (throwable != null) { + finishedFuture.completeExceptionally(throwable); + } else { + finishedFuture.complete(null); + } + } + + @Override + public void onError(final Throwable t) { + finishedFuture.completeExceptionally(t); + doneReading(); + } + + private final CompletionHandler writeCompletionHandler = new CompletionHandler() { + @Override + public void completed() { + decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally(); + } + + @Override + public void failed(final Throwable t) { + finishedFuture.completeExceptionally(t); + decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally(); + } + }; +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java new file mode 100644 index 00000000000..b0781c402d5 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java @@ -0,0 +1,213 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.BindingNotFoundException; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.service.BindingSetNotFoundException; + +import javax.annotation.concurrent.GuardedBy; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author tonytv + */ +public class ServletResponseController { + private static Logger log = Logger.getLogger(ServletResponseController.class.getName()); + + /** + * The servlet spec does not require (Http)ServletResponse nor ServletOutputStream to be thread-safe. Therefore, + * we must provide our own synchronization, since we may attempt to access these objects simultaneously from + * different threads. (The typical cause of this is when one thread is writing a response while another thread + * throws an exception, causing the request to fail with an error response). + */ + private final Object monitor = new Object(); + + //servletResponse must not be modified after the response has been committed. + private final HttpServletResponse servletResponse; + private final boolean developerMode; + + //all calls to the servletOutputStreamWriter must hold the monitor first to ensure visibility of servletResponse changes. + private final ServletOutputStreamWriter servletOutputStreamWriter; + + @GuardedBy("monitor") + private boolean responseCommitted = false; + + + public ServletResponseController( + HttpServletResponse servletResponse, + Executor executor, + MetricReporter metricReporter, + boolean developerMode) throws IOException { + + this.servletResponse = servletResponse; + this.developerMode = developerMode; + this.servletOutputStreamWriter = + new ServletOutputStreamWriter(servletResponse.getOutputStream(), executor, metricReporter); + } + + + private static int getStatusCode(Throwable t) { + if (t instanceof BindingNotFoundException) { + return HttpServletResponse.SC_NOT_FOUND; + } else if (t instanceof BindingSetNotFoundException) { + return HttpServletResponse.SC_NOT_FOUND; + } else if (t instanceof RequestException) { + return ((RequestException)t).getResponseStatus(); + } else { + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + } + + private static String getReasonPhrase(Throwable t, boolean developerMode) { + if (developerMode) { + final StringWriter out = new StringWriter(); + t.printStackTrace(new PrintWriter(out)); + return out.toString(); + } else if (t.getMessage() != null) { + return t.getMessage(); + } else { + return t.toString(); + } + } + + + public void trySendError(Throwable t) { + final boolean responseWasCommitted; + + synchronized (monitor) { + responseWasCommitted = responseCommitted; + + if (!responseCommitted) { + responseCommitted = true; + servletOutputStreamWriter.setSendingError(); + } + } + + //Must be evaluated after state transition for test purposes(See ConformanceTestException) + //Done outside the monitor since it causes a callback in tests. + String reasonPhrase = getReasonPhrase(t, developerMode); + int statusCode = getStatusCode(t); + + if (responseWasCommitted) { + + RuntimeException exceptionWithStackTrace = new RuntimeException(t); + log.log(Level.FINE, "Response already committed, can't change response code", exceptionWithStackTrace); + // TODO: should always have failed here, but that breaks test assumptions. Doing soft close instead. + //assert !Thread.holdsLock(monitor); + //servletOutputStreamWriter.fail(t); + servletOutputStreamWriter.close(null); + return; + } + + try { + servletResponse.sendError( + statusCode, + reasonPhrase); + finishedFuture().complete(null); + } catch (Throwable e) { + servletOutputStreamWriter.fail(t); + } + } + + /** + * When this future completes there will be no more calls against the servlet output stream or servlet response. + * The framework is still allowed to invoke us though. + * + * The future might complete in the servlet framework thread, user thread or executor thread. + */ + public CompletableFuture<Void> finishedFuture() { + return servletOutputStreamWriter.finishedFuture; + } + + private void setResponse(Response jdiscResponse) { + synchronized (monitor) { + if (responseCommitted) { + log.log(Level.FINE, + jdiscResponse.getError(), + () -> "Response already committed, can't change response code. " + + "From: " + servletResponse.getStatus() + ", To: " + jdiscResponse.getStatus()); + + //TODO: should throw an exception here, but this breaks unit tests. + //The failures will now instead happen when writing buffers. + servletOutputStreamWriter.close(null); + return; + } + + setStatus_holdingLock(jdiscResponse, servletResponse); + setHeaders_holdingLock(jdiscResponse, servletResponse); + } + } + + private static void setHeaders_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) { + for (final Map.Entry<String, String> entry : jdiscResponse.headers().entries()) { + final String value = entry.getValue(); + servletResponse.addHeader(entry.getKey(), value != null ? value : ""); + } + + if (servletResponse.getContentType() == null) { + servletResponse.setContentType("text/plain;charset=utf-8"); + } + } + + private static void setStatus_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) { + if (jdiscResponse instanceof HttpResponse) { + servletResponse.setStatus(jdiscResponse.getStatus(), ((HttpResponse) jdiscResponse).getMessage()); + } else { + Optional<String> errorMessage = getErrorMessage(jdiscResponse); + if (errorMessage.isPresent()) { + servletResponse.setStatus(jdiscResponse.getStatus(), errorMessage.get()); + } else { + servletResponse.setStatus(jdiscResponse.getStatus()); + } + } + } + + private static Optional<String> getErrorMessage(Response jdiscResponse) { + return Optional.ofNullable(jdiscResponse.getError()).flatMap( + error -> Optional.ofNullable(error.getMessage())); + } + + + private void commitResponse() { + synchronized (monitor) { + responseCommitted = true; + } + } + + public final ResponseHandler responseHandler = new ResponseHandler() { + @Override + public ContentChannel handleResponse(Response response) { + setResponse(response); + return responseContentChannel; + } + }; + + public final ContentChannel responseContentChannel = new ContentChannel() { + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + commitResponse(); + servletOutputStreamWriter.writeBuffer(buf, handler); + } + + @Override + public void close(CompletionHandler handler) { + commitResponse(); + servletOutputStreamWriter.close(handler); + } + }; +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java new file mode 100644 index 00000000000..1a34a3b81c3 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.net.URI; + +/** + * @author tonytv + */ +public class UnsupportedFilterInvoker implements FilterInvoker { + @Override + public HttpServletRequest invokeRequestFilterChain(RequestFilter requestFilterChain, + URI uri, + HttpServletRequest httpRequest, + ResponseHandler responseHandler) { + throw new UnsupportedOperationException(); + } + + @Override + public void invokeResponseFilterChain( + ResponseFilter responseFilterChain, + URI uri, + HttpServletRequest request, + HttpServletResponse response) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java new file mode 100644 index 00000000000..8c15582c80e --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java @@ -0,0 +1,419 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; + +import javax.annotation.concurrent.GuardedBy; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + * @since 5.17.0 + */ +class WebSocketRequestDispatch extends WebSocketAdapter { + + private final static Logger log = Logger.getLogger(WebSocketRequestDispatch.class.getName()); + private final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); + + private final AtomicReference<Object> responseRef = new AtomicReference<>(); + private final CurrentContainer container; + private final Executor janitor; + private final RequestHandler requestHandler; + private final Metric metric; + private final Metric.Context metricCtx; + private final Object lock = new Object(); + private final CompletionHandler failureHandlingCompletionHandler = new CompletionHandler() { + @Override + public void completed() { + } + + @Override + public void failed(final Throwable t) { + synchronized (lock) { + fail_holdingLock(t); + } + } + }; + + @GuardedBy("lock") + private final Deque<ResponseContentPart> responseContentQueue = new ArrayDeque<>(); + @GuardedBy("lock") + private ContentChannel requestContent; + @GuardedBy("lock") + private Throwable failure; + @GuardedBy("lock") + private boolean writingResponse = false; + @GuardedBy("lock") + private boolean connected; + + public WebSocketRequestDispatch( + final CurrentContainer container, + final Executor janitor, + final Metric metric, + final Metric.Context metricCtx) { + Objects.requireNonNull(janitor, "janitor"); + Objects.requireNonNull(metric, "metric"); + this.container = container; + this.requestHandler = new AbstractRequestHandler() { + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return request.connect(handler); + } + }; + this.janitor = janitor; + this.metric = metric; + this.metricCtx = metricCtx; + } + + public WebSocketRequestDispatch dispatch(final ServletUpgradeRequest servletRequest, + final ServletUpgradeResponse servletResponse) { + final HttpRequest jdiscRequest = WebSocketRequestFactory.newJDiscRequest(container, servletRequest); + try (final ResourceReference ref = References.fromResource(jdiscRequest)) { + WebSocketRequestFactory.copyHeaders(servletRequest, jdiscRequest); + dispatchRequestWithoutThrowing(jdiscRequest); + } + final Response jdiscResponse = (Response)responseRef.getAndSet(new Object()); + if (jdiscResponse != null) { + log.finer("Applying sync " + jdiscResponse.getStatus() + " response to websocket response."); + servletResponse.setStatus(jdiscResponse.getStatus()); + WebSocketRequestFactory.copyHeaders(jdiscResponse, servletResponse); + } + return this; + } + + @Override + public void onWebSocketBinary(final byte[] arr, final int off, final int len) { + writeRequestContentWithoutThrowing(ByteBuffer.wrap(arr, off, len)); + } + + @Override + public void onWebSocketText(final String message) { + writeRequestContentWithoutThrowing(StandardCharsets.UTF_8.encode(message)); + } + + @Override + public void onWebSocketConnect(final Session session) { + super.onWebSocketConnect(session); + synchronized (lock) { + connected = true; + if (writingResponse) { + return; + } + writingResponse = true; + } + writeNextResponseContent(); + } + + /** + * This is ALWAYS called. + * ...if the remote side closes the connection + * ...if we c*ck up ourselves and throw an exception out of onWebSocketBinary() or onWebSocketText(), + * Jetty calls Session.close on our behalf (later followed by a call to onWebSocketError) + * + * TODO: Test below + * ...and also whenever we call Session.close() ourselves?? + * + * @param statusCode The {@link StatusCode} of the close. + * @param reason The reason text for the close. + */ + @Override + public void onWebSocketClose(final int statusCode, final String reason) { + super.onWebSocketClose(statusCode, reason); + final ContentChannel requestContentChannel; + synchronized (lock) { + Preconditions.checkState(requestContent != null || failure != null, + "requestContent should be non-null if we haven't had a failure"); + if (requestContent == null) { + return; + } + if (failure != null) { + // Request content will be closed as a result of the failure handling. + return; + } + requestContentChannel = requestContent; + requestContent = null; + } + try { + requestContentChannel.close(failureHandlingCompletionHandler); + } catch (final Throwable t) { + fail(t); + } + } + + /** + * <p>No need to call Session.close() here, that has been done or will be done by Jetty.</p> + * + * @param t The cause of the error. + */ + @Override + public void onWebSocketError(final Throwable t) { + fail(t); + } + + private void dispatchRequestWithoutThrowing(final Request request) { + final ContentChannel returnedContentChannel; + try { + returnedContentChannel = requestHandler.handleRequest(request, new GatedResponseHandler()); + } catch (final Throwable t) { + fail(t); + throw new IllegalStateException(t); + } + synchronized (lock) { + Preconditions.checkState(requestContent == null, "requestContent should be null"); + if (failure != null) { + // This means that request.connect() caused a synchronous failure. in this case + // the cleanup happened before requestContent was assigned, so we must clean it explicitly here + closeLater(returnedContentChannel); + throw new IllegalStateException(failure); + } + requestContent = returnedContentChannel; + } + } + + private void writeRequestContentWithoutThrowing(final ByteBuffer buf) { + int bytes_received = buf.remaining(); + metric.set(JettyHttpServer.Metrics.NUM_BYTES_RECEIVED, bytes_received, metricCtx); + metric.set(JettyHttpServer.Metrics.MANHATTAN_NUM_BYTES_RECEIVED, bytes_received, metricCtx); + final ContentChannel requestContentChannel; + synchronized (lock) { + Preconditions.checkState(requestContent != null, "requestContent should be non-null"); + if (failure != null) { + return; + } + requestContentChannel = requestContent; + } + try { + requestContentChannel.write(buf, failureHandlingCompletionHandler); + } catch (final Throwable t) { + fail(t); + } + } + + private void fail(final Throwable t) { + synchronized (lock) { + fail_holdingLock(t); + } + } + + private void tryWriteResponseContent(final ByteBuffer buf, final CompletionHandler handler) { + synchronized (lock) { + if (failure != null) { + failLater(handler, failure); + return; + } + responseContentQueue.addLast(new ResponseContentPart(buf, handler)); + if (writingResponse) { + return; + } + writingResponse = true; + } + writeNextResponseContent(); + } + + private void writeNextResponseContent() { + while (true) { + final ResponseContentPart part; + synchronized (lock) { + if (!connected) { + // We expect a later invocation of onWebSocketConnect(). That will invoke this method again. + writingResponse = false; + return; + } + if (responseContentQueue.isEmpty()) { + writingResponse = false; + return; // application will call later + } + part = responseContentQueue.poll(); + } + if (part.handler != null) { + try { + part.handler.completed(); + } catch (final Throwable t) { + fail(t); + return; + } + } + final boolean isClosePart = part.buf == null; + if (isClosePart) { + return; + } + try { + getRemote().sendBytesByFuture(part.buf); + } catch (final Throwable t) { + fail(t); + } + } + } + + private void fail_holdingLock(final Throwable failure) { + if (this.failure != null) { + return; + } + this.failure = failure; + if (requestContent != null) { + closeLater(requestContent); + } + requestContent = null; + for (ResponseContentPart part = responseContentQueue.poll(); part != null; part = responseContentQueue.poll()) { + failLater(part.handler, failure); + } + janitor.execute(() -> { + try { + getSession().close(StatusCode.SERVER_ERROR, failure.toString()); + } catch (final Throwable ignored) { + } + }); + } + + private void closeLater(final ContentChannel content) { + janitor.execute(() -> { + try { + content.close(NOOP_COMPLETION_HANDLER); + } catch (final Throwable ignored) { + } + }); + } + + private void failLater(final CompletionHandler handler, final Throwable failure) { + if (handler == null) { + return; + } + + final Throwable failureWithStack = new IllegalStateException(failure); + janitor.execute(() -> { + try { + handler.failed(failureWithStack); + } catch (final Throwable t) { + log.log(Level.WARNING, "Failure handling of " + failure + + " in application threw an exception.", t); + } + }); + } + + private static final CompletionHandler NOOP_COMPLETION_HANDLER = new CompletionHandler() { + @Override public void completed() {} + @Override public void failed(final Throwable t) {} + }; + + private class GatedResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(final Response response) { + synchronized (lock) { + if (failure != null) { + return new FailedResponseContent(new IllegalStateException(failure)); + } + } + final boolean firstToSetResponse = responseRef.compareAndSet(null, response); + if (!firstToSetResponse) { + log.finer("Ignoring async " + response.getStatus() + " response because sync websocket response has " + + "already been returned to client."); + // TODO(bakksjo): The message above is not necessarily correct. Getting here does not necessarily + // mean that a sync response has been returned to the client. It may just mean that dispatch() is + // finished, and the request handler's handleRequest() has been run. That does not mean that the + // request handler actually produced a sync response. If a response is produced asynchronously, we + // may get here and ignore that response. TODO: Analyze wire traffic. Maybe Jetty produces a response + // after dispatch(), even if we don't do it in our code. Besides, is the status code used for anything + // by the client anyway? Is it even available in client WebSocket implementations? + } + return new GatedResponseContent(); + } + } + + private class GatedResponseContent implements ContentChannel { + + @Override + public void write(final ByteBuffer raw, final CompletionHandler handler) { + final ByteBuffer buf = raw != null ? raw : EMPTY_BUFFER; + int bytesSent = buf.remaining(); + metric.set(JettyHttpServer.Metrics.NUM_BYTES_SENT, bytesSent, metricCtx); + metric.set(JettyHttpServer.Metrics.MANHATTAN_NUM_BYTES_SENT, bytesSent, metricCtx); + tryWriteResponseContent(buf, new MetricCompletionHandler(handler)); + } + + @Override + public void close(final CompletionHandler handler) { + // The only reason to let this synthetic 'part' go into the queue is to have the completion handler + // for close() invoked in order (after the completion handlers for enqueued parts. + tryWriteResponseContent(null, new MetricCompletionHandler(handler)); + } + } + + private class FailedResponseContent implements ContentChannel { + + final Throwable failure; + + FailedResponseContent(final Throwable failure) { + this.failure = failure; + } + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + failLater(new MetricCompletionHandler(handler), failure); + } + + @Override + public void close(final CompletionHandler handler) { + failLater(new MetricCompletionHandler(handler), failure); + } + } + + private class MetricCompletionHandler implements CompletionHandler { + + final CompletionHandler delegate; + + MetricCompletionHandler(CompletionHandler delegate) { + this.delegate = delegate; + } + + @Override + public void completed() { + metric.add(JettyHttpServer.Metrics.NUM_SUCCESSFUL_WRITES, 1, metricCtx); + if (delegate != null) + delegate.completed(); + } + + @Override + public void failed(Throwable t) { + metric.add(JettyHttpServer.Metrics.NUM_FAILED_WRITES, 1, metricCtx); + if (delegate != null) + delegate.failed(t); + } + } + + private static class ResponseContentPart { + + final ByteBuffer buf; + final CompletionHandler handler; + + ResponseContentPart(final ByteBuffer buf, final CompletionHandler handler) { + this.buf = buf; + this.handler = handler; + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java new file mode 100644 index 00000000000..8eebc11ce75 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; + +import java.net.InetSocketAddress; +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class WebSocketRequestFactory { + + public static HttpRequest newJDiscRequest(final CurrentContainer container, + final ServletUpgradeRequest servletRequest) { + return HttpRequest.newServerRequest( + container, + servletRequest.getRequestURI(), + HttpRequest.Method.valueOf(servletRequest.getMethod()), + HttpRequest.Version.fromString(servletRequest.getHttpVersion()), + new InetSocketAddress(servletRequest.getRemoteAddress(), servletRequest.getRemotePort())); + } + + public static void copyHeaders(final ServletUpgradeRequest from, final Request to) { + to.headers().addAll(from.getHeaders()); + } + + public static void copyHeaders(final Response from, final ServletUpgradeResponse to) { + for (final Map.Entry<String, String> entry : from.headers().entries()) { + to.addHeader(entry.getKey(), entry.getValue()); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java new file mode 100644 index 00000000000..acebb1707a8 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java @@ -0,0 +1,3 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@com.yahoo.osgi.annotation.ExportPackage +package com.yahoo.jdisc.http.server.jetty; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java new file mode 100644 index 00000000000..0fd783bc939 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java @@ -0,0 +1,4 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.server; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java new file mode 100644 index 00000000000..d98749c4cde --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.servlet; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; + +import java.net.SocketAddress; +import java.net.URI; +import java.util.List; +import java.util.Map; + +/** + * Common interface for JDisc and servlet http requests. + */ +public interface ServletOrJdiscHttpRequest { + + public void copyHeaders(HeaderFields target); + + public Map<String, List<String>> parameters(); + + public URI getUri(); + + public HttpRequest.Version getVersion(); + + public String getRemoteHostAddress(); + public String getRemoteHostName(); + public int getRemotePort(); + + public void setRemoteAddress(SocketAddress remoteAddress); + + public Map<String, Object> context(); + + public List<Cookie> decodeCookieHeader(); + + public void encodeCookieHeader(List<Cookie> cookies); +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java new file mode 100644 index 00000000000..afcb2861b1e --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.servlet; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; + +import java.util.List; +import java.util.Map; + +/** + * Common interface for JDisc and servlet http responses. + */ +public interface ServletOrJdiscHttpResponse { + + public void copyHeaders(HeaderFields target); + + public int getStatus(); + + public Map<String, Object> context(); + + public List<Cookie> decodeSetCookieHeader(); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java new file mode 100644 index 00000000000..d4213452677 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java @@ -0,0 +1,245 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.servlet; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Mutable wrapper to use a {@link javax.servlet.http.HttpServletRequest} + * with JDisc security filters. + * <p> + * You might find it tempting to remove e.g. the getParameter... methods, + * but keep in mind that this IS-A servlet request and must provide the + * full api of such a request for use outside the "JDisc filter world". + * + * @since 5.27 + */ +public class ServletRequest extends HttpServletRequestWrapper implements ServletOrJdiscHttpRequest { + + private final HttpServletRequest request; + private final HeaderFields headerFields; + private final Set<String> headerBlacklist = new HashSet<>(); + private final Map<String, Object> context = new HashMap<>(); + private final Map<String, List<String>> parameters = new HashMap<>(); + + private URI uri; + private String remoteHostAddress; + private String remoteHostName; + private int remotePort; + + public ServletRequest(HttpServletRequest request, URI uri) { + super(request); + this.request = request; + + this.uri = uri; + + super.getParameterMap().forEach( + (key, values) -> parameters.put(key, Arrays.asList(values))); + + remoteHostAddress = request.getRemoteAddr(); + remoteHostName = request.getRemoteHost(); + remotePort = request.getRemotePort(); + + headerFields = new HeaderFields(); + Enumeration<String> parentHeaders = request.getHeaderNames(); + while (parentHeaders.hasMoreElements()) { + String name = parentHeaders.nextElement(); + Enumeration<String> values = request.getHeaders(name); + while (values.hasMoreElements()) { + headerFields.add(name, values.nextElement()); + } + } + } + + public HttpServletRequest getRequest() { + return request; + } + + @Override + public Map<String, List<String>> parameters() { + return parameters; + } + + /* We cannot just return the parameter map from the request, as the map + * may have been modified by the JDisc filters. */ + @Override + public Map<String, String[]> getParameterMap() { + Map<String, String[]> parameterMap = new HashMap<>(); + parameters().forEach( + (key, values) -> + parameterMap.put(key, values.toArray(new String[values.size()])) + ); + return ImmutableMap.copyOf(parameterMap); + } + + @Override + public String getParameter(String name) { + return parameters().containsKey(name) ? + parameters().get(name).get(0) : + null; + } + + @Override + public Enumeration<String> getParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + List<String> values = parameters().get(name); + return values != null ? + values.toArray(new String[values.size()]) : + null; + } + + @Override + public void copyHeaders(HeaderFields target) { + target.addAll(headerFields); + } + + @Override + public Enumeration<String> getHeaders(String name) { + if (headerBlacklist.contains(name)) + return null; + + /* We don't need to merge headerFields and the servlet request's headers + * because setHeaders() replaces the old value. There is no 'addHeader(s)'. */ + List<String> headerFields = this.headerFields.get(name); + return headerFields == null || headerFields.isEmpty() ? + super.getHeaders(name) : + Collections.enumeration(headerFields); + } + + @Override + public String getHeader(String name) { + if (headerBlacklist.contains(name)) + return null; + + String headerField = headerFields.getFirst(name); + return headerField != null ? + headerField : + super.getHeader(name); + } + + @Override + public Enumeration<String> getHeaderNames() { + Set<String> names = new HashSet<>(Collections.list(super.getHeaderNames())); + names.addAll(headerFields.keySet()); + names.removeAll(headerBlacklist); + return Collections.enumeration(names); + } + + public void addHeader(String name, String value) { + headerFields.add(name, value); + headerBlacklist.remove(name); + } + + public void setHeaders(String name, String value) { + headerFields.put(name, value); + headerBlacklist.remove(name); + } + + public void setHeaders(String name, List<String> values) { + headerFields.put(name, values); + headerBlacklist.remove(name); + } + + public void removeHeaders(String name) { + headerFields.remove(name); + headerBlacklist.add(name); + } + + @Override + public URI getUri() { + return uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + @Override + public HttpRequest.Version getVersion() { + String protocol = request.getProtocol(); + try { + return HttpRequest.Version.fromString(protocol); + } catch (NullPointerException | IllegalArgumentException e) { + throw new RuntimeException("Servlet request protocol '" + protocol + + "' could not be mapped to a JDisc http version.", e); + } + } + + @Override + public String getRemoteHostAddress() { + return remoteHostAddress; + } + + @Override + public String getRemoteHostName() { + return remoteHostName; + } + + @Override + public int getRemotePort() { + return remotePort; + } + + @Override + public void setRemoteAddress(SocketAddress remoteAddress) { + if (remoteAddress instanceof InetSocketAddress) { + remoteHostAddress = ((InetSocketAddress) remoteAddress).getAddress().getHostAddress(); + remoteHostName = ((InetSocketAddress) remoteAddress).getAddress().getHostName(); + remotePort = ((InetSocketAddress) remoteAddress).getPort(); + } else + throw new RuntimeException("Unknown SocketAddress class: " + remoteHostAddress.getClass().getName()); + + } + + @Override + public Map<String, Object> context() { + return context; + } + + @Override + public javax.servlet.http.Cookie[] getCookies() { + return decodeCookieHeader().stream(). + map(jdiscCookie -> new javax.servlet.http.Cookie(jdiscCookie.getName(), jdiscCookie.getValue())). + toArray(javax.servlet.http.Cookie[]::new); + } + + @Override + public List<Cookie> decodeCookieHeader() { + Enumeration<String> cookies = getHeaders(HttpHeaders.Names.COOKIE); + if (cookies == null) + return Collections.emptyList(); + + List<Cookie> ret = new LinkedList<>(); + while(cookies.hasMoreElements()) + ret.addAll(Cookie.fromCookieHeader(cookies.nextElement())); + + return ret; + } + + @Override + public void encodeCookieHeader(List<Cookie> cookies) { + setHeaders(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies)); + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java new file mode 100644 index 00000000000..be5a3f67886 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.servlet; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * JDisc wrapper to use a {@link javax.servlet.http.HttpServletResponse} + * with JDisc security filters. + * + * @since 5.26 + */ +public class ServletResponse extends HttpServletResponseWrapper implements ServletOrJdiscHttpResponse { + + private final HttpServletResponse response; + private final Map<String, Object> context = new HashMap<>(); + + public ServletResponse(HttpServletResponse response) { + super(response); + this.response = response; + } + + public HttpServletResponse getResponse() { + return response; + } + + @Override + public int getStatus() { + return response.getStatus(); + } + + @Override + public Map<String, Object> context() { + return context; + } + + @Override + public void copyHeaders(HeaderFields target) { + response.getHeaderNames().forEach( header -> + target.add(header, new ArrayList<>(response.getHeaders(header))) + ); + } + + @Override + public List<Cookie> decodeSetCookieHeader() { + Collection<String> cookies = getHeaders(HttpHeaders.Names.SET_COOKIE); + if (cookies == null) { + return Collections.emptyList(); + } + List<Cookie> ret = new LinkedList<>(); + for (String cookie : cookies) { + ret.addAll(Cookie.fromSetCookieHeader(cookie)); + } + return ret; + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java new file mode 100644 index 00000000000..8aa50caac99 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.servlet; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java new file mode 100644 index 00000000000..d9eebbeedc6 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +/** + * @author tonytv + */ +public class JKSKeyStore extends SslKeyStore { + + private static final String keyStoreType = "JKS"; + private final Path keyStoreFile; + + public JKSKeyStore(Path keyStoreFile) { + this.keyStoreFile = keyStoreFile; + } + + @Override + public KeyStore loadJavaKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + try(InputStream stream = Files.newInputStream(keyStoreFile)) { + KeyStore keystore = KeyStore.getInstance(keyStoreType); + keystore.load(stream, getKeyStorePassword().map(String::toCharArray).orElse(null)); + return keystore; + } + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java new file mode 100644 index 00000000000..8a3ac08a1cd --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import java.io.Reader; +import java.nio.file.Path; + +/** + * A reader along with the path used to construct it. + * + * @author tonytv + */ +public final class ReaderForPath { + + public final Reader reader; + public final Path path; + + public ReaderForPath(Reader reader, Path path) { + this.reader = reader; + this.path = path; + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java new file mode 100644 index 00000000000..93cf6683ed5 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java @@ -0,0 +1,88 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:charlesk@yahoo-inc.com">Charles Kim</a> + */ +public class SslContextFactory { + + private static final Logger log = Logger.getLogger(SslContextFactory.class.getName()); + private static final String DEFAULT_ALGORITHM = "SunX509"; + private static final String DEFAULT_PROTOCOL = "TLS"; + private final SSLContext sslContext; + + private SslContextFactory(SSLContext sslContext) { + this.sslContext = sslContext; + } + + public SSLContext getServerSSLContext() { + return this.sslContext; + } + + public static SslContextFactory newInstanceFromTrustStore(SslKeyStore trustStore) { + return newInstance(DEFAULT_ALGORITHM, DEFAULT_PROTOCOL, null, trustStore); + } + + public static SslContextFactory newInstance(SslKeyStore trustStore, SslKeyStore keyStore) { + return newInstance(DEFAULT_ALGORITHM, DEFAULT_PROTOCOL, keyStore, trustStore); + } + + public static SslContextFactory newInstance(String sslAlgorithm, String sslProtocol, + SslKeyStore keyStore, SslKeyStore trustStore) { + log.fine("Configuring SSLContext..."); + log.fine("Using " + sslAlgorithm + " algorithm."); + try { + SSLContext sslContext = SSLContext.getInstance(sslProtocol); + sslContext.init( + keyStore == null ? null : getKeyManagers(keyStore, sslAlgorithm), + trustStore == null ? null : getTrustManagers(trustStore, sslAlgorithm), + null); + return new SslContextFactory(sslContext); + } catch (Exception e) { + log.log(Level.SEVERE, "Got exception creating SSLContext.", e); + throw new RuntimeException(e); + } + } + + /** + * Used for the key store, which contains the SSL cert and private key. + */ + public static javax.net.ssl.KeyManager[] getKeyManagers(SslKeyStore keyStore, + String sslAlgorithm) + throws NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, + KeyStoreException { + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(sslAlgorithm); + keyManagerFactory.init( + keyStore.loadJavaKeyStore(), + keyStore.getKeyStorePassword().map(String::toCharArray).orElse(null)); + log.fine("KeyManagerFactory initialized with keystore"); + return keyManagerFactory.getKeyManagers(); + } + + /** + * Used for the trust store, which contains certificates from other parties that you expect to communicate with, + * or from Certificate Authorities that you trust to identify other parties. + */ + public static javax.net.ssl.TrustManager[] getTrustManagers(SslKeyStore trustStore, + String sslAlgorithm) + throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException { + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(sslAlgorithm); + trustManagerFactory.init(trustStore.loadJavaKeyStore()); + log.fine("TrustManagerFactory initialized with truststore."); + return trustManagerFactory.getTrustManagers(); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java new file mode 100644 index 00000000000..de65618a942 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Optional; + +/** + * + * @author <a href="mailto:charlesk@yahoo-inc.com">Charles Kim</a> + */ +public abstract class SslKeyStore { + + private Optional<String> keyStorePassword = Optional.empty(); + + public Optional<String> getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = Optional.of(keyStorePassword); + } + + public abstract KeyStore loadJavaKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException; + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java new file mode 100644 index 00000000000..4d5a5b1c806 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl; + +import java.nio.file.Paths; + +/** + * A factory for SSL key stores. + * + * @author bratseth + */ +public interface SslKeyStoreFactory { + + SslKeyStore createKeyStore(ReaderForPath certificateFile, ReaderForPath keyFile); + + SslKeyStore createTrustStore(ReaderForPath certificateFile); + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java new file mode 100644 index 00000000000..251a355d19b --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java @@ -0,0 +1,4 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.jdisc.http.ssl; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java new file mode 100644 index 00000000000..f045dbb0dca --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java @@ -0,0 +1,124 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ChunkReader { + + private static final Pattern CONTENT_LENGTH = Pattern.compile(".+^content-length: (\\d+)$.*", + Pattern.CASE_INSENSITIVE | + Pattern.MULTILINE | + Pattern.DOTALL); + private static final Pattern CHUNKED_ENCODING = Pattern.compile(".+^transfer-encoding: chunked$.*", + Pattern.CASE_INSENSITIVE | + Pattern.MULTILINE | + Pattern.DOTALL); + private final InputStream in; + private StringBuilder reading = new StringBuilder(); + private boolean readingHeader = true; + + public ChunkReader(InputStream in) { + this.in = in; + } + + public boolean isEndOfContent() throws IOException { + if (in.available() != 0) { + StringBuilder sb = new StringBuilder(); + sb.append(in.available()).append(": "); + for(int c = in.read(); c != -1; c = in.read()) { + sb.append('\''); + sb.append(c); + sb.append("' "); + } + throw new IllegalStateException("This is not the end '" + sb.toString()); + } + return in.available() == 0; + } + + public String readChunk() throws IOException { + while (true) { + String ret = removeNextChunk(); + if (ret != null) { + return ret; + } + readFromStream(); + } + } + + private String readContent(int length) throws IOException { + while (reading.length() < length) { + readFromStream(); + } + return splitReadBuffer(length); + } + + private void readFromStream() throws IOException { + byte[] buf = new byte[4096]; + try { + while (!Thread.currentThread().isInterrupted()) { + int len = in.read(buf, 0, buf.length); + if (len < 0) { + throw new IOException("Socket is closed."); + } + if (len > 0) { + reading.append(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(buf, 0, len))); + break; + } + Thread.sleep(10); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private String removeNextChunk() throws IOException { + if (readingHeader) { + int pos = reading.indexOf("\r\n\r\n"); + if (pos < 0) { + return null; + } + String ret = splitReadBuffer(pos + 4); + Matcher m = CONTENT_LENGTH.matcher(ret); + if (m.matches()) { + ret += readContent(Integer.valueOf(m.group(1))); + } + readingHeader = !CHUNKED_ENCODING.matcher(ret).matches(); + return ret; + } else if (reading.indexOf("0\r\n") == 0) { + int pos = reading.indexOf("\r\n\r\n", 1); + if (pos < 0) { + return null; + } + readingHeader = true; + return splitReadBuffer(pos + 4); + } else { + int pos = reading.indexOf("\r\n"); + if (pos < 0) { + return null; + } + pos = reading.indexOf("\r\n", pos + 2); + if (pos < 0) { + return null; + } + return splitReadBuffer(pos + 2); + } + } + + private String splitReadBuffer(int pos) { + String ret = reading.substring(0, pos); + if (pos < reading.length()) { + reading = new StringBuilder(reading.substring(pos)); + } else { + reading = new StringBuilder(); + } + return ret; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java new file mode 100644 index 00000000000..1a5553fb608 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java @@ -0,0 +1,119 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import com.ning.http.util.AllowAllHostnameVerifier; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.http.client.HttpClient; +import com.yahoo.jdisc.http.client.HttpClientConfig; +import com.yahoo.jdisc.http.client.filter.ResponseFilter; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; + +import javax.net.ssl.HostnameVerifier; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ClientTestDriver { + + private final TestDriver driver; + private final HttpClient client; + private final RemoteServer server; + + private ClientTestDriver(TestDriver driver, HttpClient client) throws IOException { + this.driver = driver; + this.client = client; + this.server = RemoteServer.newInstance(); + } + + public CurrentContainer currentContainer() { + return driver; + } + + public boolean close() { + if (!server.close(60, TimeUnit.SECONDS)) { + return false; + } + client.release(); + return driver.close(); + } + + public HttpClient client() { + return client; + } + + public RemoteServer server() { + return server; + } + + public static ClientTestDriver newInstance(Module... guiceModules) throws IOException { + return newInstance(new HttpClientConfig.Builder().sslConnectionPoolEnabled(false), + guiceModules); + } + + public static ClientTestDriver newInstance(HttpClientConfig.Builder config, Module... guiceModules) + throws IOException { + Module[] lst = new Module[guiceModules.length + 2]; + lst[0] = newDefaultModule(); + lst[lst.length - 1] = newConfigModule(config); + System.arraycopy(guiceModules, 0, lst, 1, guiceModules.length); + return newInstanceImpl(HttpClient.class, lst); + } + + private static ClientTestDriver newInstanceImpl(Class<? extends HttpClient> clientClass, + Module... guiceModules) throws IOException { + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(guiceModules); + ContainerBuilder builder = driver.newContainerBuilder(); + HttpClient client = builder.guiceModules().getInstance(clientClass); + builder.serverBindings().bind("*://*/*", client); + driver.activateContainer(builder); + try { + client.start(); + } catch (RuntimeException e) { + client.release(); + driver.close(); + throw e; + } + return new ClientTestDriver(driver, client); + } + + public static Module newDefaultModule() { + return new AbstractModule() { + + @Override + protected void configure() { + bind(HostnameVerifier.class).to(AllowAllHostnameVerifier.class); + bind(new TypeLiteral<List<ResponseFilter>>() { }).toInstance(Collections.<ResponseFilter>emptyList()); + } + }; + } + + public static Module newConfigModule(final HttpClientConfig.Builder config) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(HttpClientConfig.class).toInstance(new HttpClientConfig(config)); + } + }; + } + + public static Module newFilterModule(final ResponseFilter... filters) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(new TypeLiteral<List<ResponseFilter>>() { }).toInstance(Arrays.asList(filters)); + } + }; + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java new file mode 100644 index 00000000000..f752ac86dd0 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import java.io.IOException; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static com.yahoo.jdisc.http.test.ServerTestDriver.newFilterModule; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * + * TODO: dead code? + */ +public class FilterTestDriver { + + private final ServerTestDriver driver; + private final MyRequestHandler requestHandler; + + private FilterTestDriver(ServerTestDriver driver, MyRequestHandler requestHandler) { + this.driver = driver; + this.requestHandler = requestHandler; + } + + public boolean close() throws IOException { + return driver.close(); + } + + public HttpRequest filterRequest(String request) throws IOException, TimeoutException, InterruptedException { + driver.client().writeRequest(request); + return (HttpRequest)requestHandler.exchanger.exchange(null, 60, TimeUnit.SECONDS); + } + + public static FilterTestDriver newInstance(final BindingRepository<RequestFilter> requestFilters, + final BindingRepository<ResponseFilter> responseFilters) + throws IOException { + MyRequestHandler handler = new MyRequestHandler(); + return new FilterTestDriver(ServerTestDriver.newInstance(handler, + newFilterModule(requestFilters, responseFilters)), + handler); + } + + private static class MyRequestHandler extends AbstractRequestHandler { + + final Exchanger<Request> exchanger = new Exchanger<>(); + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler); + try { + exchanger.exchange(request); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return null; + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java new file mode 100644 index 00000000000..e2c1a2a33d5 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import com.yahoo.jdisc.http.server.jetty.JettyHttpServer; +import com.yahoo.jdisc.http.ssl.SslContextFactory; +import com.yahoo.jdisc.http.ssl.SslKeyStore; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class RemoteClient extends ChunkReader { + + private final Socket socket; + + private RemoteClient(Socket socket) throws IOException { + super(socket.getInputStream()); + this.socket = socket; + } + + public void close() throws IOException { + socket.close(); + } + + public void writeRequest(String request) throws IOException { + socket.getOutputStream().write(request.getBytes(StandardCharsets.UTF_8)); + } + + public static RemoteClient newInstance(JettyHttpServer server) throws IOException { + return newInstance(server.getListenPort()); + } + + public static RemoteClient newInstance(int listenPort) throws IOException { + return new RemoteClient(new Socket("localhost", listenPort)); + } + + public static RemoteClient newSslInstance(int listenPort, SslKeyStore sslKeyStore) throws IOException { + SSLContext ctx = SslContextFactory.newInstanceFromTrustStore(sslKeyStore).getServerSSLContext(); + if (ctx == null) { + throw new RuntimeException("Failed to create socket with SSLContext."); + } + return new RemoteClient(ctx.getSocketFactory().createSocket("localhost", listenPort)); + } + + public static RemoteClient newSslInstance(JettyHttpServer server, SslKeyStore keyStore) throws IOException { + return newSslInstance(server.getListenPort(), keyStore); + } + +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java new file mode 100644 index 00000000000..75368549ae6 --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java @@ -0,0 +1,110 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RemoteServer implements Runnable { + + private final Thread thread = new Thread(this, "RemoteServer@" + System.identityHashCode(this)); + private final LinkedBlockingQueue<Socket> clients = new LinkedBlockingQueue<>(); + private final ServerSocket server; + + private RemoteServer(int listenPort) throws IOException { + this.server = new ServerSocket(listenPort); + } + + @Override + public void run() { + try { + while (!Thread.interrupted()) { + Socket client = server.accept(); + if (client != null) { + clients.add(client); + } + } + } catch (IOException e) { + if (!server.isClosed()) { + e.printStackTrace(); + } + } + } + + public URI newRequestUri(String uri) { + return newRequestUri(URI.create(uri)); + } + + public URI newRequestUri(URI uri) { + URI serverUri = connectionSpec(); + try { + return new URI(serverUri.getScheme(), serverUri.getUserInfo(), serverUri.getHost(), + serverUri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public URI connectionSpec() { + return URI.create("http://localhost:" + server.getLocalPort() + "/"); + } + + public Connection awaitConnection(int timeout, TimeUnit unit) throws InterruptedException, IOException { + Socket client = clients.poll(timeout, unit); + if (client == null) { + return null; + } + return new Connection(client); + } + + public boolean close(int timeout, TimeUnit unit) { + try { + server.close(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + try { + thread.join(unit.toMillis(timeout)); + } catch (InterruptedException e) { + return false; + } + return !thread.isAlive(); + } + + public static RemoteServer newInstance() throws IOException { + RemoteServer ret = new RemoteServer(0); + ret.thread.start(); + return ret; + } + + public static class Connection extends ChunkReader { + + private final Socket socket; + private final PrintWriter out; + + private Connection(Socket socket) throws IOException { + super(socket.getInputStream()); + this.socket = socket; + this.out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); + } + + public void writeChunk(String chunk) { + out.print(chunk); + } + + public void close() throws IOException { + out.close(); + socket.close(); + } + } +} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java new file mode 100644 index 00000000000..17a2b6ee6ee --- /dev/null +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java @@ -0,0 +1,146 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.server.jetty.JettyHttpServer; +import com.yahoo.jdisc.http.ssl.SslKeyStore; +import com.yahoo.jdisc.test.TestDriver; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ServerTestDriver { + + private final TestDriver driver; + private final JettyHttpServer server; + private final RemoteClient client; + + private ServerTestDriver(TestDriver driver, JettyHttpServer server, RemoteClient client) { + this.driver = driver; + this.server = server; + this.client = client; + } + + public boolean close() throws IOException { + client.close(); + server.close(); + server.release(); + return driver.close(); + } + + public TestDriver parent() { + return driver; + } + + public ContainerActivator containerActivator() { + return driver; + } + + public JettyHttpServer server() { + return server; + } + + public RemoteClient client() { + return client; + } + + public HttpRequest newRequest(HttpRequest.Method method, String uri, HttpRequest.Version version) { + return HttpRequest.newServerRequest(driver, newRequestUri(uri), method, version); + } + + public URI newRequestUri(String uri) { + return newRequestUri(URI.create(uri)); + } + + public URI newRequestUri(URI uri) { + try { + return new URI("http", null, "locahost", + server.getListenPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public static ServerTestDriver newInstance(RequestHandler requestHandler, Module... guiceModules) throws IOException { + return newInstance(requestHandler, Arrays.asList(guiceModules)); + } + + public static ServerTestDriver newInstance(RequestHandler requestHandler, Iterable<Module> guiceModules) + throws IOException { + List<Module> lst = new LinkedList<>(); + lst.add(newDefaultModule()); + for (Module module : guiceModules) { + lst.add(module); + } + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(lst.toArray(new Module[lst.size()])); + ContainerBuilder builder = driver.newContainerBuilder(); + builder.serverBindings().bind("*://*/*", requestHandler); + JettyHttpServer server = builder.guiceModules().getInstance(JettyHttpServer.class); + return newInstance(null, driver, builder, server); + } + + private static ServerTestDriver newInstance(SslKeyStore clientTrustStore, TestDriver driver, ContainerBuilder builder, + JettyHttpServer server) throws IOException { + builder.serverProviders().install(server); + driver.activateContainer(builder); + try { + server.start(); + } catch (RuntimeException e) { + server.release(); + driver.close(); + throw e; + } + RemoteClient client; + if (clientTrustStore == null) { + client = RemoteClient.newInstance(server); + } else { + client = RemoteClient.newSslInstance(server, clientTrustStore); + } + return new ServerTestDriver(driver, server, client); + } + + public static Module newDefaultModule() { + return new AbstractModule() { + + @Override + protected void configure() { + bind(new TypeLiteral<BindingRepository<RequestFilter>>() { }) + .toInstance(new BindingRepository<>()); + bind(new TypeLiteral<BindingRepository<ResponseFilter>>() { }) + .toInstance(new BindingRepository<>()); + } + }; + } + + public static Module newFilterModule(final BindingRepository<RequestFilter> requestFilters, + final BindingRepository<ResponseFilter> responseFilters) { + return new AbstractModule() { + + @Override + protected void configure() { + if (requestFilters != null) { + bind(new TypeLiteral<BindingRepository<RequestFilter>>() { }).toInstance(requestFilters); + } + if (responseFilters != null) { + bind(new TypeLiteral<BindingRepository<ResponseFilter>>() { }).toInstance(responseFilters); + } + } + }; + } +} diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def new file mode 100644 index 00000000000..8f2b4dfd86f --- /dev/null +++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def @@ -0,0 +1,36 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http.client + +userAgent string default = "JDisc/1.0" +chunkedEncodingEnabled bool default = false +compressionEnabled bool default = false +connectionPoolEnabled bool default = true +followRedirects bool default = false +removeQueryParamsOnRedirect bool default = true +sslConnectionPoolEnabled bool default = true +proxyServer string default = "" +useProxyProperties bool default = false +useRawUri bool default = false +compressionLevel int default = -1 +maxNumConnections int default = -1 +maxNumConnectionsPerHost int default = -1 +maxNumRedirects int default = 5 +maxNumRetries int default = 0 +connectionTimeout double default = 60 +idleConnectionInPoolTimeout double default = 60 +idleConnectionTimeout double default = 60 +idleWebSocketTimeout double default = 15 +requestTimeout double default = 60 + +ssl.enabled bool default = false +ssl.keyStoreType string default = "JKS" + +# Vespa home is prepended is path is relative +ssl.keyStorePath string default = "jdisc_container/keyStore.jks" + +# Vespa home is prepended is path is relative +ssl.trustStorePath string default = "conf/jdisc_container/trustStore.jks" + +ssl.keyDBKey string default = "jdisc_container" +ssl.algorithm string default = "SunX509" +ssl.protocol string default = "TLS" diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def new file mode 100644 index 00000000000..3e71212449e --- /dev/null +++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def @@ -0,0 +1,79 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http + +# The TCP port to listen to for this connector. +listenPort int default=0 + +# The connector name +name string default="default" + +# The header field cache size. +headerCacheSize int default=512 + +# The size of the buffer into which response content is aggregated before being sent to the client. +outputBufferSize int default=65536 + +# The maximum size of a request header. +requestHeaderSize int default=65536 + +# The maximum size of a response header. +responseHeaderSize int default=65536 + +# The accept queue size (also known as accept backlog). +acceptQueueSize int default=0 + +# Whether the server socket reuses addresses. +reuseAddress bool default=true + +# The linger time. Use -1 to disable. +soLingerTime int default=-1 + +# The maximum idle time for a connection, which roughly translates to the Socket.setSoTimeout(int). +idleTimeout double default=180.0 + +# The stop timeout. +stopTimeout double default=30.0 + +# Whether or not to have socket keep alive turned on. +tcpKeepAliveEnabled bool default=false + +# Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm). +tcpNoDelay bool default=true + +# Whether to enable SSL for this connector. +ssl.enabled bool default=false + +# The KeyDB key. +ssl.keyDbKey string default="" + +# Names of protocols to exclude. +ssl.excludeProtocol[].name string + +# Names of protocols to include. +ssl.includeProtocol[].name string + +# Names of cipher suites to exclude. +ssl.excludeCipherSuite[].name string + +# Names of cipher suites to include. +ssl.includeCipherSuite[].name string + +# The type of the keystore. +ssl.keyStoreType enum { JKS, PEM } default=JKS + +# JKS only - the path to the keystore. +ssl.keyStorePath string default="" + +ssl.pemKeyStore.keyPath string default="" +ssl.pemKeyStore.certificatePath string default="" + +ssl.trustStoreType enum { JKS } default="JKS" + +# JKS only - the path to the truststore. +ssl.trustStorePath string default="" + +# The algorithm name used by the KeyManagerFactory. +ssl.sslKeyManagerFactoryAlgorithm string default="SunX509" + +# The SSL protocol passed to SSLContext.getInstance() +ssl.protocol string default="TLS" diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def new file mode 100644 index 00000000000..cfcd440939d --- /dev/null +++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def @@ -0,0 +1,23 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http + +# Whether to enable developer mode, where stack traces etc are visible in response bodies. +developerMode bool default=false + +# The gzip compression level to use, if compression is enabled in a request. +responseCompressionLevel int default=6 + +# Whether to enable HTTP keep-alive for requests that support this. +httpKeepAliveEnabled bool default=true + +# Whether the request body of POSTed forms should be removed (form parameters are available as request parameters). +removeRawPostBodyForWwwUrlEncodedPost bool default=false + +# The component ID of a filter +filter[].id string + +# The binding of a filter +filter[].binding string + +# Max number of threads in pool +maxWorkerThreads int default = 200 diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def new file mode 100644 index 00000000000..4cc93d2b7e4 --- /dev/null +++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def @@ -0,0 +1,5 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http + +# path by servlet componentId +servlets{}.path string diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java new file mode 100644 index 00000000000..9c85164dcb6 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * @author <a href="mailto:apurvak@yahoo-inc.com">Apurva Kumar</a> + */ + +public class AssertFile { + + public static void assertContains(File logFile, String expected) throws IOException { + String s = new String( + Files.readAllBytes(Paths.get(logFile.getAbsolutePath())), + StandardCharsets.UTF_8); + assertThat(s, containsString(expected)); + } + + public static void assertNotContains(File logFile, String expected) throws IOException { + String s = new String( + Files.readAllBytes(Paths.get(logFile.getAbsolutePath())), + StandardCharsets.UTF_8); + assertThat(s, not(containsString(expected))); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java new file mode 100644 index 00000000000..b60c269ac98 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.test.RemoteClient; +import com.yahoo.jdisc.http.test.ServerTestDriver; + +import java.io.IOException; +import java.util.Arrays; +import java.util.regex.Pattern; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public abstract class AssertHttp { + + public static void assertChunk(String expected, String actual) { + if (expected.startsWith("HTTP/1.")) { + expected = sortChunk(expected); + actual = sortChunk(actual); + } + Pattern pattern = Pattern.compile(expected, Pattern.DOTALL | Pattern.MULTILINE); + if (pattern.matcher(actual).matches()) { + return; + } + assertEquals(expected, actual); + } + + public static void assertResponse(RequestHandler requestHandler, String request, + String... expectedChunks) throws IOException { + ServerTestDriver driver = ServerTestDriver.newInstance(requestHandler); + assertResponse(driver, request, expectedChunks); + assertTrue(driver.close()); + } + + public static void assertResponse(ServerTestDriver driver, String request, String... expectedChunks) + throws IOException { + assertResponse(driver.client(), request, expectedChunks); + } + + public static void assertResponse(RemoteClient client, String request, String... expectedChunks) + throws IOException { + client.writeRequest(request); + for (String expected : expectedChunks) { + assertChunk(expected, client.readChunk()); + } + } + + private static String sortChunk(String chunk) { + String[] lines = chunk.split("\r\n"); + if (lines.length > 2) { + int prev = 1, next = 2; + for ( ; next < lines.length && !lines[next].isEmpty(); ++next) { + if (!Character.isLetterOrDigit(lines[next].charAt(0))) { + Arrays.sort(lines, prev, next); + prev = next + 1; + } + } + if (prev < next) { + Arrays.sort(lines, prev, next); + } + } + StringBuilder out = new StringBuilder(); + for (String line : lines) { + out.append(line).append("\r\n"); + } + return out.toString(); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java new file mode 100644 index 00000000000..de2b0d453e6 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java @@ -0,0 +1,315 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import org.jboss.netty.handler.codec.http.CookieDecoder; +import org.jboss.netty.handler.codec.http.DefaultCookie; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNotSame; +import static org.testng.AssertJUnit.assertSame; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class CookieTestCase { + + @Test + public void requireThatDefaultValuesAreSane() { + assertCookie(new DefaultCookie("foo", "bar"), new Cookie().setName("foo").setValue("bar")); + assertCookie(new DefaultCookie("foo", "bar"), new Cookie("foo", "bar")); + } + + @Test + public void requireThatAccessorsWork() { + final Cookie cookie = new Cookie(); + cookie.setName("foo"); + assertEquals("foo", cookie.getName()); + cookie.setName("bar"); + assertEquals("bar", cookie.getName()); + + cookie.setValue("foo"); + assertEquals("foo", cookie.getValue()); + cookie.setValue("bar"); + assertEquals("bar", cookie.getValue()); + + cookie.setDomain("foo"); + assertEquals("foo", cookie.getDomain()); + cookie.setDomain("bar"); + assertEquals("bar", cookie.getDomain()); + + cookie.setPath("foo"); + assertEquals("foo", cookie.getPath()); + cookie.setPath("bar"); + assertEquals("bar", cookie.getPath()); + + cookie.setComment("foo"); + assertEquals("foo", cookie.getComment()); + cookie.setComment("bar"); + assertEquals("bar", cookie.getComment()); + + cookie.setCommentUrl("foo"); + assertEquals("foo", cookie.getCommentUrl()); + assertSame(cookie.getCommentUrl(), cookie.getCommentURL()); + cookie.setCommentUrl("bar"); + assertEquals("bar", cookie.getCommentUrl()); + assertSame(cookie.getCommentUrl(), cookie.getCommentURL()); + + cookie.setMaxAge(69, TimeUnit.DAYS); + assertEquals(69, cookie.getMaxAge(TimeUnit.DAYS)); + assertEquals(TimeUnit.DAYS.toHours(69), cookie.getMaxAge(TimeUnit.HOURS)); + cookie.setVersion(69); + assertEquals(69, cookie.getVersion()); + + cookie.setSecure(true); + assertTrue(cookie.isSecure()); + cookie.setSecure(false); + assertFalse(cookie.isSecure()); + + cookie.setHttpOnly(true); + assertTrue(cookie.isHttpOnly()); + cookie.setHttpOnly(false); + assertFalse(cookie.isHttpOnly()); + + cookie.setDiscard(true); + assertTrue(cookie.isDiscard()); + cookie.setDiscard(false); + assertFalse(cookie.isDiscard()); + + cookie.ports().add(6); + assertEquals(1, cookie.ports().size()); + assertTrue(cookie.ports().contains(6)); + cookie.ports().add(9); + assertEquals(2, cookie.ports().size()); + assertTrue(cookie.ports().contains(6)); + assertTrue(cookie.ports().contains(9)); + } + + @Test + public void requireThatCopyConstructorWorks() { + final Cookie lhs = newCookie("foo"); + final Cookie rhs = new Cookie(lhs); + assertEquals(rhs.getName(), rhs.getName()); + assertEquals(rhs.getValue(), rhs.getValue()); + assertEquals(rhs.getDomain(), rhs.getDomain()); + assertEquals(rhs.getPath(), rhs.getPath()); + assertEquals(rhs.getComment(), rhs.getComment()); + assertEquals(rhs.getCommentUrl(), rhs.getCommentUrl()); + assertEquals(rhs.getMaxAge(TimeUnit.MILLISECONDS), rhs.getMaxAge(TimeUnit.MILLISECONDS)); + assertEquals(rhs.getVersion(), rhs.getVersion()); + assertEquals(rhs.isSecure(), rhs.isSecure()); + assertEquals(rhs.isHttpOnly(), rhs.isHttpOnly()); + assertEquals(rhs.isDiscard(), rhs.isDiscard()); + assertEquals(rhs.ports(), lhs.ports()); + assertNotSame(rhs.ports(), lhs.ports()); + } + + @Test + public void requireThatHashCodeIsImplemented() { + final Cookie cookie = newCookie("foo"); + assertFalse(cookie.hashCode() == new Cookie().hashCode()); + assertEquals(cookie.hashCode(), cookie.hashCode()); + assertEquals(cookie.hashCode(), new Cookie(cookie).hashCode()); + } + + @Test + public void requireThatEqualsIsImplemented() { + final Cookie cookie = newCookie("foo"); + assertFalse(cookie.equals(new Cookie())); + assertEquals(cookie, cookie); + assertEquals(cookie, new Cookie(cookie)); + } + + @Test + public void requireThatCookieCanBeEncoded() { + assertEncodeCookie( + Collections.singletonList("$Version=1; foo.name=foo.value; $Path=path; $Domain=domain; $Port=\"69\""), + Collections.singletonList(newCookie("foo"))); + assertEncodeCookie( + Arrays.asList("$Version=1; bar.name=bar.value; $Path=path; $Domain=domain; $Port=\"69\"", + "$Version=1; foo.name=foo.value; $Path=path; $Domain=domain; $Port=\"69\""), + Arrays.asList(newCookie("foo"), newCookie("bar"))); + } + + @Test + public void requireThatSetCookieCanBeEncoded() { + assertEncodeSetCookie( + Collections.singletonList("foo.name=foo.value; Max-Age=0; Path=path; Domain=domain; Secure; " + + "HTTPOnly; Comment=comment; Version=1; CommentURL=\"commentUrl\"; " + + "Port=\"69\"; Discard"), + Collections.singletonList(newCookie("foo"))); + } + + @Test + public void requireThatOnlyOneSetCookieCanBeEncoded() { + try { + Cookie.toSetCookieHeader(Arrays.asList(newCookie("foo"), newCookie("bar"))); + fail(); + } catch (final IllegalStateException ignored) { + + } + } + + @Test + public void requireThatCookieCanBeDecoded() { + final Cookie foo = new Cookie(); + foo.setName("foo.name"); + foo.setValue("foo.value"); + foo.setVersion(1); + foo.setPath("path"); + foo.setDomain("domain"); + foo.setMaxAge(-1, TimeUnit.SECONDS); + assertDecodeSetCookie(Collections.singletonList(foo), + "$Version=1;foo.name=foo.value;$Path=path;$Domain=domain;$Port=\"69\""); + + final Cookie bar = new Cookie(); + bar.setName("bar.name"); + bar.setValue("bar.value"); + bar.setVersion(1); + bar.setPath("path"); + bar.setDomain("domain"); + bar.setMaxAge(-1, TimeUnit.SECONDS); + assertDecodeCookie(Arrays.asList(foo, bar), + "$Version=1;foo.name=foo.value;$Path=path;$Domain=domain;$Port=\"69\";" + + "$Version=1;bar.name=bar.value;$Path=path;$Domain=domain;$Port=\"69\";"); + } + + @Test + public void requireThatSetCookieCanBeDecoded() { + final Cookie foo = new Cookie(); + foo.setName("foo.name"); + foo.setValue("foo.value"); + foo.setVersion(1); + foo.setPath("path"); + foo.setDomain("domain"); + foo.setMaxAge(-1, TimeUnit.SECONDS); + assertDecodeSetCookie(Collections.singletonList(foo), + "foo.name=foo.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;Comment=comment;" + + "Version=2;CommentURL=\"commentUrl\";Port=\"69\";Discard"); + + final Cookie bar = new Cookie(); + bar.setName("bar.name"); + bar.setValue("bar.value"); + bar.setVersion(1); + bar.setPath("path"); + bar.setDomain("domain"); + bar.setMaxAge(-1, TimeUnit.SECONDS); + assertDecodeSetCookie(Arrays.asList(foo, bar), + "bar.name=bar.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;Comment=comment;" + + "Version=2;CommentURL=\"commentUrl\";Port=\"69\";Discard;" + + "foo.name=foo.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;Comment=comment;" + + "Version=2;CommentURL=\"commentUrl\";Port=\"69\";Discard"); + } + + @Test + public void requireThatCookieDecoderWorksForGenericValidCookies() { + new CookieDecoder().decode("Y=v=1&n=8es5opih9ljtk&l=og0_iedeh0qqvqqr/o&p=m2g2rs6012000000&r=pv&lg=en-US&intl=" + + "us&np=1; T=z=h.nzPBhSP4PBVd5JqacVnIbNjU1NAY2TjYzNzVOTjYzNzM0Mj&a=YAE&sk=DAALShmNQ" + + "vhoZV&ks=EAABsibvMK6ejwn0uUoS4rC9w--~E&d=c2wBTVRJeU13RXhPVEUwTURJNU9URTBNRFF6TlRJ" + + "NU5nLS0BYQFZQUUBZwE1VkNHT0w3VUVDTklJVEdRR1FXT0pOSkhEQQFzY2lkAWNOUnZIbEc3ZHZoVHlWZ" + + "0NoXzEwYkxhOVdzcy0Bb2sBWlcwLQF0aXABWUhwTmVDAXp6AWgubnpQQkE3RQ--"); + } + + @Test + public void requireThatCookieDecoderWorksForYInvalidCookies() { + new CookieDecoder().decode("Y=v=1&n=77nkr5t7o4nqn&l=og0_iedeh0qqvqqr/o&p=m2g2rs6012000000&r=pv&lg=en-US&intl=" + + "us&np=1; T=z=05nzPB0NP4PBN/n0gwc1AWGNjU1NAY2TjYzNzVOTjYzNzM0Mj&a=QAE&sk=DAA4R2svo" + + "osjIa&ks=EAAj3nBQFkN4ZmuhqFxJdNoaQ--~E&d=c2wBTVRJeU13RXhPVEUwTURJNU9URTBNRFF6TlRJ" + + "NU5nLS0BYQFRQUUBZwE1VkNHT0w3VUVDTklJVEdRR1FXT0pOSkhEQQFzY2lkAUpPalRXOEVsUDZrR3RHT" + + "VZkX29CWk53clJIQS0BdGlwAVlIcE5lQwF6egEwNW56UEJBN0U-"); + } + + @Test + public void requireThatCookieDecoderWorksForYValidCookies() { + new CookieDecoder().decode("Y=v=1&n=3767k6te5aj2s&l=1v4u3001uw2ys00q0rw0qrw34q0x5s3u/o&p=030vvit012000000&iz=" + + "&r=pu&lg=en-US,it-IT,it&intl=it&np=1; T=z=m38yPBmLk3PBWvehTPBhBHYNU5OBjQ3NE5ONU5P" + + "NDY0NzU0M0&a=IAE&sk=DAAAx5URYgbhQ6&ks=EAA4rTgdlAGeMQmdYeM_VehGg--~E&d=c2wBTWprNUF" + + "UTXdNems1TWprNE16RXpNREl6TkRneAFhAUlBRQFnAUVJSlNMSzVRM1pWNVNLQVBNRkszQTRaWDZBAXNj" + + "aWQBSUlyZW5paXp4NS4zTUZMMDVlSVhuMjZKYUcwLQFvawFaVzAtAWFsAW1hcmlvYXByZWFAeW1haWwuY" + + "29tAXp6AW0zOHlQQkE3RQF0aXABaXRZOFRE"); + } + + @Test + public void requireThatCookieDecoderWorksForGenericInvalidCookies() { + new CookieDecoder().decode("Y=v=1&n=e92s5cq8qbs6h&l=3kdb0f.3@i126be10b.d4j/o&p=m1f2qgmb13000107&r=g5&lg=en-US" + + "&intl=us; T=z=TXp3OBTrQ8OBFMcj3GBpFSyNk83TgY2MjMwN04zMDMw&a=YAE&sk=DAAVfaNwLeISrX" + + "&ks=EAAOeNNgY8c5hV8YzPYmnrW7w--~E&d=c2wBTVRnd09RRXhOVFEzTURrME56UTMBYQFZQUUBZwFMQ" + + "U5NT0Q2UjY2Q0I1STY0R0tKSUdVQVlRRQFvawFaVzAtAXRpcAFMTlRUdkMBenoBVFhwM09CQTdF&af=QU" + + "FBQ0FDQURBd0FCMUNCOUFJQUJBQ0FEQU1IME1nTWhNbiZ0cz0xMzIzMjEwMTk1JnBzPVA1d3NYakh0aVk" + + "2UDMuUGZ6WkdTT2ctLQ--"); + } + + private static void assertDecodeCookie(final List<Cookie> expected, final String toDecode) { + assertCookies(expected, Cookie.fromCookieHeader(toDecode)); + } + + private static void assertDecodeSetCookie(final List<Cookie> expected, final String toDecode) { + assertCookies(expected, Cookie.fromSetCookieHeader(toDecode)); + } + + private static void assertCookies(final List<Cookie> expected, final List<Cookie> actual) { + assertEquals(expected.size(), actual.size()); + for (final Cookie cookie : expected) { + assertNotNull(actual.remove(cookie)); + } + } + + private static void assertEncodeCookie(final List<String> expected, final List<Cookie> toEncode) { + assertCookies(expected, Cookie.toCookieHeader(toEncode)); + } + + private static void assertEncodeSetCookie(final List<String> expected, final List<Cookie> toEncode) { + assertCookies(expected, Cookie.toSetCookieHeader(toEncode)); + } + + private static void assertCookies(final List<String> expected, final String actual) { + final Set<Integer> seen = new HashSet<>(); + for (final String str : expected) { + final int pos = actual.indexOf(str); + assertTrue(pos >= 0); + assertTrue(seen.add(pos)); + } + } + + private static void assertCookie(final DefaultCookie expected, final Cookie actual) { + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getValue(), actual.getValue()); + assertEquals(expected.getDomain(), actual.getDomain()); + assertEquals(expected.getPath(), actual.getPath()); + assertEquals(expected.getComment(), actual.getComment()); + assertEquals(expected.getCommentUrl(), actual.getCommentUrl()); + assertEquals(expected.getMaxAge(), actual.getMaxAge(TimeUnit.SECONDS)); + assertEquals(expected.getVersion(), actual.getVersion()); + assertEquals(expected.isSecure(), actual.isSecure()); + assertEquals(expected.isHttpOnly(), actual.isHttpOnly()); + assertEquals(expected.isDiscard(), actual.isDiscard()); + } + + private static Cookie newCookie(final String name) { + final Cookie cookie = new Cookie(); + cookie.setName(name + ".name"); + cookie.setValue(name + ".value"); + cookie.setDomain("domain"); + cookie.setPath("path"); + cookie.setComment("comment"); + cookie.setCommentUrl("commentUrl"); + cookie.setMaxAge(69, TimeUnit.MILLISECONDS); + cookie.setVersion(2); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookie.setDiscard(true); + cookie.ports().add(69); + return cookie; + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java new file mode 100644 index 00000000000..2cfd1563b45 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.application.MetricConsumer; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author <a href="mailto:ssameer@yahoo-inc.com">ssameer</a> + * Date: 2/15/13 + * Time: 11:49 AM + */ +public class DummyMetricManager extends AbstractModule implements MetricConsumer { + + private final Map<String, Integer> metrics = new HashMap<>(); + private Map<String, ?> lastContextDimensions; + + @Override + protected void configure() { + bind(MetricConsumer.class).toInstance(this); + } + + @Override + public void add(String key, Number val, Metric.Context ctx) { + synchronized (metrics) { + metrics.put(key, get(key) + val.intValue()); + } + } + + @Override + public void set(String key, Number val, Metric.Context ctx) { + synchronized (metrics) { + metrics.put(key, val.intValue()); + } + } + + @Override + public Metric.Context createContext(Map<String, ?> dimensions) { + lastContextDimensions = dimensions; + return new Metric.Context() { }; + } + + public Map<String, ?> getLastContextDimensions() { + return lastContextDimensions; + } + + public int get(String key) { + Integer val; + synchronized (metrics) { + val = metrics.get(key); + } + return val != null ? val : 0; + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java new file mode 100644 index 00000000000..1472c411c38 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import org.testng.annotations.Test; + +import static org.testng.AssertJUnit.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class HttpHeadersTestCase { + + @Test + public void requireThatHeadersDoNotChange() { + assertEquals("X-JDisc-Disable-Chunking", HttpHeaders.Names.X_DISABLE_CHUNKING); + assertEquals("X-JDisc-Enable-TraceId", HttpHeaders.Names.X_ENABLE_TRACE_ID); + assertEquals("X-JDisc-TraceId", HttpHeaders.Names.X_TRACE_ID); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java new file mode 100644 index 00000000000..021a14b2ae7 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java @@ -0,0 +1,249 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.test.TestDriver; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.testng.annotations.Test; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HttpRequestTestCase { + + @Test + public void requireThatMethodIsCompatibleWithNetty() { + assertMethod(HttpRequest.Method.OPTIONS, HttpMethod.OPTIONS); + assertMethod(HttpRequest.Method.GET, HttpMethod.GET); + assertMethod(HttpRequest.Method.HEAD, HttpMethod.HEAD); + assertMethod(HttpRequest.Method.POST, HttpMethod.POST); + assertMethod(HttpRequest.Method.PUT, HttpMethod.PUT); + assertMethod(HttpRequest.Method.PATCH, HttpMethod.PATCH); + assertMethod(HttpRequest.Method.DELETE, HttpMethod.DELETE); + assertMethod(HttpRequest.Method.TRACE, HttpMethod.TRACE); + assertMethod(HttpRequest.Method.CONNECT, HttpMethod.CONNECT); + assertEquals(9, HttpRequest.Method.values().length); + } + + @Test + public void requireThatVersionIsCompatibleWithNetty() { + assertVersion(HttpRequest.Version.HTTP_1_0, HttpVersion.HTTP_1_0); + assertVersion(HttpRequest.Version.HTTP_1_1, HttpVersion.HTTP_1_1); + assertEquals(2, HttpRequest.Version.values().length); + } + + @Test + public void requireThatSimpleServerConstructorsUseReasonableDefaults() { + final URI uri = URI.create("http://localhost/"); + HttpRequest request = HttpRequest.newServerRequest(mockContainer(), uri); + assertTrue(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.GET, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.POST); + assertTrue(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.POST, HttpRequest.Version.HTTP_1_0); + assertTrue(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); + } + + @Test + public void requireThatSimpleClientConstructorsUseReasonableDefaults() { + final Request parent = new Request(mockContainer(), URI.create("http://localhost/")); + + final URI uri = URI.create("http://remotehost/"); + HttpRequest request = HttpRequest.newClientRequest(parent, uri); + assertFalse(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.GET, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newClientRequest(parent, uri, HttpRequest.Method.POST); + assertFalse(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newClientRequest(parent, uri, HttpRequest.Method.POST, HttpRequest.Version.HTTP_1_0); + assertFalse(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); + } + + @Test + public void requireThatAccessorsWork() { + URI uri = URI.create("http://localhost/path?foo=bar&foo=baz&cox=69"); + InetSocketAddress address = new InetSocketAddress("remotehost", 69); + final HttpRequest request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.GET, + HttpRequest.Version.HTTP_1_1, address, 1L); + assertEquals(uri, request.getUri()); + request.setUri(uri = URI.create("http://remotehost/")); + assertEquals(uri, request.getUri()); + + assertEquals(HttpRequest.Method.GET, request.getMethod()); + request.setMethod(HttpRequest.Method.CONNECT); + assertEquals(HttpRequest.Method.CONNECT, request.getMethod()); + + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + request.setVersion(HttpRequest.Version.HTTP_1_0); + assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); + + assertEquals(address, request.getRemoteAddress()); + request.setRemoteAddress(address = new InetSocketAddress("localhost", 96)); + assertEquals(address, request.getRemoteAddress()); + + final URI proxy = URI.create("http://proxyhost/"); + request.setProxyServer(proxy); + assertEquals(proxy, request.getProxyServer()); + + assertNull(request.getConnectionTimeout(TimeUnit.MILLISECONDS)); + request.setConnectionTimeout(1, TimeUnit.SECONDS); + assertEquals(Long.valueOf(1000), request.getConnectionTimeout(TimeUnit.MILLISECONDS)); + + assertEquals(Arrays.asList("bar", "baz"), request.parameters().get("foo")); + assertEquals(Collections.singletonList("69"), request.parameters().get("cox")); + request.parameters().put("cox", Arrays.asList("6", "9")); + assertEquals(Arrays.asList("bar", "baz"), request.parameters().get("foo")); + assertEquals(Arrays.asList("6", "9"), request.parameters().get("cox")); + + assertEquals(1L, request.getConnectedAt(TimeUnit.MILLISECONDS)); + } + + @Test + public void requireThatHttp10EncodingIsNeverChunked() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.isChunked()); + request.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); + assertFalse(request.isChunked()); + } + + @Test + public void requireThatHttp11EncodingIsNotChunkedByDefault() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + assertFalse(request.isChunked()); + } + + @Test + public void requireThatHttp11EncodingCanBeChunked() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + request.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); + assertTrue(request.isChunked()); + } + + @Test + public void requireThatHttp10ConnectionIsAlwaysClose() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.isKeepAlive()); + request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); + assertTrue(request.isKeepAlive()); + } + + @Test + public void requireThatHttp11ConnectionIsKeepAliveByDefault() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + assertTrue(request.isKeepAlive()); + } + + @Test + public void requireThatHttp11ConnectionCanBeClose() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE); + assertFalse(request.isKeepAlive()); + } + + @Test + public void requireThatHttp10NeverHasChunkedResponse() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.hasChunkedResponse()); + } + + @Test + public void requireThatHttp11HasDefaultChunkedResponse() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + assertTrue(request.hasChunkedResponse()); + } + + @Test + public void requireThatHttp11CanDisableChunkedResponse() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + request.headers().add(com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING, "true"); + assertFalse(request.hasChunkedResponse()); + } + + @Test + public void requireThatTraceIsDisabledByDefault() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.headers().contains(com.yahoo.jdisc.http.HttpHeaders.Names.X_ENABLE_TRACE_ID, "true")); + } + + @Test + public void requireThatCookieHeaderCanBeEncoded() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + final List<Cookie> cookies = Collections.singletonList(new Cookie("foo", "bar")); + request.encodeCookieHeader(cookies); + final List<String> headers = request.headers().get(com.yahoo.jdisc.http.HttpHeaders.Names.COOKIE); + assertEquals(1, headers.size()); + assertEquals(Cookie.toCookieHeader(cookies), headers.get(0)); + } + + @Test + public void requireThatCookieHeaderCanBeDecoded() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + final List<Cookie> cookies = Collections.singletonList(new Cookie("foo", "bar")); + request.encodeCookieHeader(cookies); + assertEquals(cookies, request.decodeCookieHeader()); + } + + private static void assertMethod(final HttpRequest.Method discMethod, final HttpMethod nettyMethod) { + assertEquals(discMethod, HttpRequest.Method.valueOf(nettyMethod.getName())); + assertEquals(discMethod, HttpRequest.Method.valueOf(nettyMethod.toString())); + assertEquals(nettyMethod, HttpMethod.valueOf(discMethod.toString())); + } + + private static void assertVersion(final HttpRequest.Version discVersion, final HttpVersion nettyVersion) { + assertEquals(discVersion, HttpRequest.Version.fromString(nettyVersion.getText())); + assertEquals(discVersion, HttpRequest.Version.fromString(nettyVersion.toString())); + assertEquals(nettyVersion, HttpVersion.valueOf(discVersion.toString())); + } + + private static HttpRequest newRequest(final HttpRequest.Version version) throws Exception { + return HttpRequest.newServerRequest( + mockContainer(), + new URI("http://localhost:1234/status.html"), + HttpRequest.Method.GET, + version); + } + + private static CurrentContainer mockContainer() { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return currentContainer; + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java new file mode 100644 index 00000000000..a6b3270002d --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java @@ -0,0 +1,142 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.service.CurrentContainer; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertSame; +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HttpResponseTestCase { + + @Test + public void requireThatAccessorsWork() throws Exception { + final HttpResponse response = newResponse(6, "foo"); + assertEquals(6, response.getStatus()); + assertEquals("foo", response.getMessage()); + assertNull(response.getError()); + assertTrue(response.isChunkedEncodingEnabled()); + + response.setStatus(9); + assertEquals(9, response.getStatus()); + + response.setMessage("bar"); + assertEquals("bar", response.getMessage()); + + final Throwable err = new Throwable(); + response.setError(err); + assertSame(err, response.getError()); + + response.setChunkedEncodingEnabled(false); + assertFalse(response.isChunkedEncodingEnabled()); + } + + @Test + public void requireThatStatusCodesDoNotChange() { + assertEquals(HttpResponse.Status.CREATED, 201); + assertEquals(HttpResponse.Status.ACCEPTED, 202); + assertEquals(HttpResponse.Status.NON_AUTHORITATIVE_INFORMATION, 203); + assertEquals(HttpResponse.Status.NO_CONTENT, 204); + assertEquals(HttpResponse.Status.RESET_CONTENT, 205); + assertEquals(HttpResponse.Status.PARTIAL_CONTENT, 206); + + assertEquals(HttpResponse.Status.MULTIPLE_CHOICES, 300); + assertEquals(HttpResponse.Status.SEE_OTHER, 303); + assertEquals(HttpResponse.Status.NOT_MODIFIED, 304); + assertEquals(HttpResponse.Status.USE_PROXY, 305); + + assertEquals(HttpResponse.Status.PAYMENT_REQUIRED, 402); + assertEquals(HttpResponse.Status.PROXY_AUTHENTICATION_REQUIRED, 407); + assertEquals(HttpResponse.Status.CONFLICT, 409); + assertEquals(HttpResponse.Status.GONE, 410); + assertEquals(HttpResponse.Status.LENGTH_REQUIRED, 411); + assertEquals(HttpResponse.Status.PRECONDITION_FAILED, 412); + assertEquals(HttpResponse.Status.REQUEST_ENTITY_TOO_LARGE, 413); + assertEquals(HttpResponse.Status.REQUEST_URI_TOO_LONG, 414); + assertEquals(HttpResponse.Status.UNSUPPORTED_MEDIA_TYPE, 415); + assertEquals(HttpResponse.Status.REQUEST_RANGE_NOT_SATISFIABLE, 416); + assertEquals(HttpResponse.Status.EXPECTATION_FAILED, 417); + + assertEquals(HttpResponse.Status.BAD_GATEWAY, 502); + assertEquals(HttpResponse.Status.GATEWAY_TIMEOUT, 504); + } + + @Test + public void requireThat5xxIsServerError() { + for (int i = 0; i < 999; ++i) { + assertEquals(i >= 500 && i < 600, HttpResponse.isServerError(new Response(i))); + } + } + + @Test + public void requireThatCookieHeaderCanBeEncoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List<Cookie> cookies = Collections.singletonList(new Cookie("foo", "bar")); + response.encodeSetCookieHeader(cookies); + final List<String> headers = response.headers().get(HttpHeaders.Names.SET_COOKIE); + assertEquals(1, headers.size()); + assertEquals(Cookie.toSetCookieHeader(cookies), + headers.get(0)); + } + + @Test + public void requireThatMultipleCookieHeadersCanBeEncoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List<Cookie> cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox")); + response.encodeSetCookieHeader(cookies); + final List<String> headers = response.headers().get(HttpHeaders.Names.SET_COOKIE); + assertEquals(2, headers.size()); + assertEquals(Cookie.toSetCookieHeader(Collections.singletonList(new Cookie("foo", "bar"))), + headers.get(0)); + assertEquals(Cookie.toSetCookieHeader(Collections.singletonList(new Cookie("baz", "cox"))), + headers.get(1)); + } + + @Test + public void requireThatCookieHeaderCanBeDecoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List<Cookie> cookies = Collections.singletonList(new Cookie("foo", "bar")); + response.encodeSetCookieHeader(cookies); + assertEquals(cookies, response.decodeSetCookieHeader()); + } + + @Test + public void requireThatMultipleCookieHeadersCanBeDecoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List<Cookie> cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox")); + response.encodeSetCookieHeader(cookies); + assertEquals(cookies, response.decodeSetCookieHeader()); + } + + private static HttpResponse newResponse(final int status, final String message) throws Exception { + final Request request = HttpRequest.newServerRequest( + mockContainer(), + new URI("http://localhost:1234/status.html"), + HttpRequest.Method.GET, + HttpRequest.Version.HTTP_1_1); + return HttpResponse.newInstance(status, message); + } + + private static CurrentContainer mockContainer() { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return currentContainer; + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java new file mode 100644 index 00000000000..1d5ab557cd1 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java @@ -0,0 +1,230 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.BufferedContentChannel; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDispatch; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.test.RemoteServer; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.jdisc.http.AssertHttp.assertChunk; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +abstract class AbstractClientTestCase { + + protected static void assertRequest(CurrentContainer container, RemoteServer server, String requestUri, + HeaderFields requestHeaders, Iterable<ByteBuffer> requestContent, + Iterable<String> expectedRequestChunks, Iterable<String> responseChunks, + int expectedStatus, String expectedMessage, + HeaderFields expectedResponseHeaders, + Iterable<ByteBuffer> expectedResponseContent, + Map<String,Object> context) throws Exception { + MyRequestDispatch dispatch = new MyRequestDispatch(container, server.newRequestUri(requestUri), + requestHeaders, requestContent, context); + dispatch.dispatch(); + assertRequest(server, expectedRequestChunks, responseChunks, dispatch); + assertResponse(dispatch.get(60, TimeUnit.SECONDS), expectedStatus, expectedMessage, + expectedResponseHeaders); + assertContent(expectedResponseContent, dispatch.responseContent.toReadable()); + } + + protected static void assertRequest(CurrentContainer container, RemoteServer server, String requestUri, + HeaderFields requestHeaders, Iterable<ByteBuffer> requestContent, + Iterable<String> expectedRequestChunks, Iterable<String> responseChunks, + int expectedStatus, String expectedMessage, + HeaderFields expectedResponseHeaders, + Iterable<ByteBuffer> expectedResponseContent) throws Exception { + + assertRequest(container, server, requestUri, requestHeaders, requestContent, expectedRequestChunks, + responseChunks, expectedStatus, expectedMessage, expectedResponseHeaders, expectedResponseContent, + Collections.<String, Object>emptyMap()); + } + + protected static void assertRequest(RemoteServer server, Iterable<String> expectedRequestChunks, + Iterable<String> responseChunks, Future<Response> futureResponse) + throws Exception { + RemoteServer.Connection cnt = awaitConnection(server, futureResponse); + assertNotNull(cnt); + for (String expected : expectedRequestChunks) { + assertChunk(expected, cnt.readChunk()); + } + for (String chunk : responseChunks) { + cnt.writeChunk(chunk); + } + cnt.close(); + } + + protected static RemoteServer.Connection awaitConnection(RemoteServer server, Future<Response> futureResponse) + throws Exception { + RemoteServer.Connection cnt = null; + for (int i = 0; i < 6000; ++i) { + cnt = server.awaitConnection(10, TimeUnit.MILLISECONDS); + if (cnt != null) { + break; + } + if (futureResponse.isDone()) { + HttpResponse response = (HttpResponse)futureResponse.get(); + System.err.println("Unexpected " + response.getStatus() + " response: " + response.getMessage()); + Throwable t = response.getError(); + if (t instanceof Exception) { + throw (Exception)t; + } else if (t instanceof Error) { + throw (Error)t; + } else { + throw new RuntimeException(t); + } + } + } + return cnt; + } + + protected static void assertResponse(Response response, int expectedStatus, String expectedMessage, + HeaderFields expectedHeaders) { + assertTrue(response instanceof HttpResponse); + HttpResponse httpResponse = (HttpResponse)response; + assertEquals(expectedStatus, httpResponse.getStatus()); + assertEquals(expectedMessage, httpResponse.getMessage()); + + HeaderFields headers = response.headers(); + for (Map.Entry<String, String> entry : expectedHeaders.entries()) { + assertTrue(headers.contains(entry.getKey(), entry.getValue())); + } + } + + protected static void assertContent(Iterable<ByteBuffer> expected, Iterable<ByteBuffer> actual) { + Iterator<ByteBuffer> expectedIt = expected.iterator(); + Iterator<ByteBuffer> actualIt = actual.iterator(); + while (expectedIt.hasNext()) { + assertTrue(actualIt.hasNext()); + assertEquals(expectedIt.next(), actualIt.next()); + } + assertFalse(actualIt.hasNext()); + } + + protected static String requestUri(String uri) { + return uri; + } + + protected static HeaderFields requestHeaders(HeaderEntry... entries) { + return asHeaders(entries); + } + + protected static Iterable<ByteBuffer> requestContent(String... chunks) { + return asContent(chunks); + } + + protected static Iterable<String> expectedRequestChunks(String... chunks) { + return Arrays.asList(chunks); + } + + protected static Iterable<String> responseChunks(String... chunks) { + return Arrays.asList(chunks); + } + + protected static int expectedResponseStatus(int status) { + return status; + } + + protected static String expectedResponseMessage(String message) { + return message; + } + + protected static HeaderFields expectedResponseHeaders(HeaderEntry... entries) { + return asHeaders(entries); + } + + protected static Iterable<ByteBuffer> expectedResponseContent(String... chunks) { + return asContent(chunks); + } + + protected static HeaderEntry newHeader(String key, String val) { + return new HeaderEntry(key, val); + } + + protected static HeaderFields asHeaders(HeaderEntry... entries) { + HeaderFields ret = new HeaderFields(); + for (HeaderEntry entry : entries) { + ret.add(entry.key, entry.val); + } + return ret; + } + + protected static Iterable<ByteBuffer> asContent(String... chunks) { + List<ByteBuffer> ret = new LinkedList<>(); + for (String chunk : chunks) { + ret.add(ByteBuffer.wrap(chunk.getBytes(StandardCharsets.UTF_8))); + } + return ret; + } + + protected static class MyRequestDispatch extends RequestDispatch { + + final Map<String, Object> context = new HashMap<>(); + final CurrentContainer container; + final URI requestUri; + final HeaderFields requestHeaders; + final Iterable<ByteBuffer> requestContent; + final BufferedContentChannel responseContent = new BufferedContentChannel(); + + MyRequestDispatch(CurrentContainer container, URI requestUri, HeaderFields requestHeaders, + Iterable<ByteBuffer> requestContent, Map<String, Object> context) { + this.container = container; + this.requestUri = requestUri; + this.requestHeaders = requestHeaders; + this.requestContent = requestContent; + this.context.putAll(context); + } + + @Override + protected Request newRequest() { + Request request = new Request(container, requestUri); + request.headers().addAll(requestHeaders); + request.context().putAll(context); + return request; + } + + @Override + protected Iterable<ByteBuffer> requestContent() { + return requestContent; + } + + @Override + public ContentChannel handleResponse(Response response) { + return responseContent; + } + } + + protected static class HeaderEntry { + + final String key; + final String val; + + HeaderEntry(String key, String val) { + this.key = key; + this.val = val; + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java new file mode 100644 index 00000000000..30cdd7acfe2 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHandler; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ReadableContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.test.NonWorkingRequest; +import org.testng.annotations.Test; + +import java.util.Map; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class AsyncResponseHandlerTestCase { + + @Test(enabled = false) + public void requireThatOnThrowableAbortsHandler() throws Exception { + AsyncResponseHandler handler = new AsyncResponseHandler(NonWorkingRequest.newInstance("http://localhost/"), + new MyResponseHandler(), new MyMetric(), + new Metric.Context() { }); + handler.onThrowable(new Throwable()); + assertEquals(AsyncHandler.STATE.ABORT, handler.onStatusReceived(null)); + assertEquals(AsyncHandler.STATE.ABORT, handler.onHeadersReceived(null)); + assertEquals(AsyncHandler.STATE.ABORT, handler.onBodyPartReceived(null)); + assertNull(handler.onCompleted()); + } + + private static class MyResponseHandler implements ResponseHandler { + + @Override + public ContentChannel handleResponse(Response response) { + return new ReadableContentChannel(); + } + } + + private static class MyMetric implements Metric { + + @Override + public void set(String key, Number val, Context ctx) { + + } + + @Override + public void add(String key, Number val, Context ctx) { + + } + + @Override + public Context createContext(Map<String, ?> properties) { + return null; + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java new file mode 100644 index 00000000000..84e758e868e --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import org.testng.annotations.Test; + +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ClientErrorTestCase { + + @Test(enabled = false) + public void requireNothing() { + assertTrue(true); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java new file mode 100644 index 00000000000..17140a88369 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java @@ -0,0 +1,108 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.ContainerThread; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestDispatch; +import com.yahoo.jdisc.http.test.ClientTestDriver; +import org.testng.annotations.Test; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ClientThreadingTestCase extends AbstractClientTestCase { + + @Test(enabled = false) + public void requireThatDefaultThreadFactoryCreatesContainerThreads() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder()); + ThreadAwareDispatch dispatch = new ThreadAwareDispatch(driver, "/foo.html"); + assertDispatch(dispatch); + assertTrue(dispatch.latch.await(60, TimeUnit.SECONDS)); + assertTrue(dispatch.thread instanceof ContainerThread); + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatThreadFactoryIsUsed() throws Exception { + final MyThreadFactory factory = new MyThreadFactory(); + ClientTestDriver driver = ClientTestDriver.newInstance(new AbstractModule() { + + @Override + protected void configure() { + bind(ThreadFactory.class).toInstance(factory); + } + }); + ThreadAwareDispatch dispatch = new ThreadAwareDispatch(driver, "/foo.html"); + assertDispatch(dispatch); + assertTrue(dispatch.latch.await(60, TimeUnit.SECONDS)); + assertTrue(factory.threads.contains(dispatch.thread)); + assertTrue(driver.close()); + } + + private static void assertDispatch(ThreadAwareDispatch dispatch) throws Exception { + dispatch.dispatch(); + assertRequest(dispatch.driver.server(), + expectedRequestChunks("POST " + dispatch.requestUri + " HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + dispatch); + assertResponse(dispatch.get(60, TimeUnit.SECONDS), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders()); + } + + private static class ThreadAwareDispatch extends RequestDispatch { + + final CountDownLatch latch = new CountDownLatch(1); + final ClientTestDriver driver; + final String requestUri; + Thread thread; + + ThreadAwareDispatch(ClientTestDriver driver, String requestUri) { + this.driver = driver; + this.requestUri = requestUri; + } + + @Override + protected Request newRequest() { + return new Request(driver.currentContainer(), driver.server().newRequestUri(requestUri)); + } + + @Override + public ContentChannel handleResponse(Response response) { + thread = Thread.currentThread(); + latch.countDown(); + return null; + } + } + + private static class MyThreadFactory implements ThreadFactory { + + final BlockingQueue<Thread> threads = new LinkedBlockingQueue<>(); + + @Override + public Thread newThread(Runnable task) { + Thread thread = new Thread(task); + threads.add(thread); + return thread; + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java new file mode 100644 index 00000000000..62b9612e59b --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertFalse; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +public class EmptyResponseTestCase { + @Test(enabled = false) + public void testGetterSetters() throws IOException { + EmptyResponse underTest = EmptyResponse.INSTANCE; + + assertEquals(0, underTest.getStatusCode()); + assertNull(underTest.getStatusText()); + assertEquals(0, underTest.getResponseBodyAsByteBuffer().remaining()); + assertEquals(0, underTest.getResponseBodyAsBytes().length); + assertNull(underTest.getResponseBodyAsStream()); + assertNull(underTest.getResponseBody()); + assertNull(underTest.getResponseBodyExcerpt(10, "")); + assertNull(underTest.getResponseBodyExcerpt(10)); + assertNull(underTest.getResponseBody()); + assertNull(underTest.getUri()); + assertNull(underTest.getContentType()); + assertNull(underTest.getHeader("")); + assertNull(underTest.getHeaders("")); + assertNull(underTest.getHeaders()); + assertFalse(underTest.isRedirected()); + assertNull(underTest.getCookies()); + assertFalse(underTest.hasResponseStatus()); + assertFalse(underTest.hasResponseHeaders()); + assertFalse(underTest.hasResponseBody()); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java new file mode 100644 index 00000000000..751b696399e --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java @@ -0,0 +1,578 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.google.inject.AbstractModule; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.MetricConsumer; +import com.yahoo.jdisc.handler.RequestDispatch; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.client.filter.FilterException; +import com.yahoo.jdisc.http.client.filter.ResponseFilter; +import com.yahoo.jdisc.http.client.filter.ResponseFilterContext; +import com.yahoo.jdisc.http.test.ClientTestDriver; +import com.yahoo.jdisc.http.test.RemoteServer; +import org.testng.annotations.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.yahoo.jdisc.http.test.ClientTestDriver.newFilterModule; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertSame; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HttpClientTestCase extends AbstractClientTestCase { + + private static final int NUM_REQUESTS = 10; + + @Test(enabled = false) + public void requireThatRequestCanBeSent() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/foo.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatRequestHeadersAreSent() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(newHeader("foo", "bar")), + requestContent(), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "foo: bar\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatRequestContentIsSent() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(), + requestContent("foo", "bar"), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "Content-Length: 6\r\n" + + "\r\n" + + "foobar"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatRequestContentCanBeChunked() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder() + .chunkedEncodingEnabled(true)); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(), + requestContent("foo", "bar"), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n", + "3\r\nfoo\r\n", + "3\r\nbar\r\n", + "0\r\n\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatGetRequestsAreNeverChunked() throws Exception { + final ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder() + .chunkedEncodingEnabled(true)); + for (int i = 0; i < NUM_REQUESTS; ++i) { + Future<Response> future = new RequestDispatch() { + + @Override + protected Request newRequest() { + return HttpRequest.newServerRequest(driver.currentContainer(), + driver.server().newRequestUri("/status.html"), + HttpRequest.Method.GET); + } + }.dispatch(); + assertRequest(driver.server(), + expectedRequestChunks("GET /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + future); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatTraceRequestsDoNotAcceptContent() throws Exception { + final ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder() + .chunkedEncodingEnabled(true)); + RequestDispatch dispatch = new RequestDispatch() { + + @Override + protected Request newRequest() { + return HttpRequest.newServerRequest(driver.currentContainer(), + driver.server().newRequestUri("/status.html"), + HttpRequest.Method.TRACE); + } + + @Override + protected Iterable<ByteBuffer> requestContent() { + return Arrays.asList(ByteBuffer.allocate(69)); + } + }; + try { + dispatch.dispatch(); + fail(); + } catch (UnsupportedOperationException e) { + + } + assertRequest(driver.server(), + expectedRequestChunks("TRACE /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + dispatch); + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatResponseCodeIsRead() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 69 foo\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(69), + expectedResponseMessage("foo"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatResponseHeadersAreRead() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "foo: bar\r\n" + + "baz: cox\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(newHeader("foo", "bar"), newHeader("baz", "cox")), + expectedResponseContent()); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatResponseContentIsRead() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Content-Length: 6\r\n" + + "\r\n" + + "foobar"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent("foobar")); + } + assertTrue(driver.close()); + } + + private void requireThatChunkedResponseContentIsRead() throws Exception { + ClientTestDriver driver = ClientTestDriver.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/status.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /status.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Connection: keep-alive\r\n" + + "\r\n", + "3\r\nfoo\r\n", + "3\r\nbar\r\n", + "0\r\n\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent("foo", "bar")); + } + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatRequestTimeoutCanOccur() throws Exception { + final ClientTestDriver driver = ClientTestDriver.newInstance(); + Response response = new RequestDispatch() { + + @Override + protected Request newRequest() { + Request request = new Request(driver.currentContainer(), driver.server().connectionSpec()); + request.setTimeout(1, TimeUnit.MILLISECONDS); + return request; + } + }.dispatch().get(60, TimeUnit.SECONDS); + assertNotNull(response); + assertEquals(Response.Status.REQUEST_TIMEOUT, response.getStatus()); + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatConnectionTimeoutCanOccur() throws Exception { + final ClientTestDriver driver = ClientTestDriver.newInstance(); + Response response = new RequestDispatch() { + + @Override + protected Request newRequest() { + HttpRequest request = HttpRequest.newServerRequest(driver.currentContainer(), + driver.server().connectionSpec()); + request.setConnectionTimeout(1, TimeUnit.MILLISECONDS); + return request; + } + }.dispatch().get(60, TimeUnit.SECONDS); + assertTrue(response instanceof HttpResponse); + HttpResponse httpResponse = (HttpResponse)response; + assertEquals(Response.Status.REQUEST_TIMEOUT, httpResponse.getStatus()); + assertEquals("java.util.concurrent.TimeoutException: No response received after 1", httpResponse.getMessage()); + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatMetricContextIsCachedPerServer() throws Exception { + MyMetric metric = new MyMetric(new Metric.Context() { + + }); + ClientTestDriver driver = ClientTestDriver.newInstance(metric); + RemoteServer server1 = driver.server(); + RemoteServer server2 = RemoteServer.newInstance(); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), server1, + requestUri("/foo.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + assertRequest(driver.currentContainer(), server2, + requestUri("/foo.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + assertTrue(server2.close(60, TimeUnit.SECONDS)); + assertEquals(2, metric.numContexts.get()); + assertTrue(metric.numCalls.get() > 0); + } + + @Test(enabled = false) + public void requireThatNullMetricContextIsLegal() throws Exception { + MyMetric metric = new MyMetric(null); + ClientTestDriver driver = ClientTestDriver.newInstance(metric); + for (int i = 0; i < NUM_REQUESTS; ++i) { + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/foo.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent()); + } + assertTrue(driver.close()); + assertEquals(1, metric.numContexts.get()); + assertTrue(metric.numCalls.get() > 0); + } + + @Test(enabled = false) + public void requireThatUnsupportedURISchemeThrowsException() throws Exception { + final ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder() + .chunkedEncodingEnabled(true) + .connectionPoolEnabled(false)); + + try { + new RequestDispatch() { + @Override + public Request newRequest() { + return HttpRequest.newServerRequest( + driver.currentContainer(), + URI.create("ftp://localhost/"), + HttpRequest.Method.GET); + } + }.dispatch(); + fail(); + } catch (UnsupportedOperationException e) { + assertEquals("Unknown protocol: ftp", e.getMessage()); + } + + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatResponseFilterIsInvoked() throws Exception { + final CountDownLatch filterInvokeCount = new CountDownLatch(2); + final StringBuffer responseBuffer = new StringBuffer(); + ResponseFilter[] filters = new ResponseFilter[2]; + filters[0] = (new ResponseFilter() { + + @Override + public ResponseFilterContext filter(ResponseFilterContext filterContext) { + filterInvokeCount.countDown(); + return filterContext; + } + }); + filters[1] = (new ResponseFilter() { + + @Override + public ResponseFilterContext filter(ResponseFilterContext filterContext) { + filterInvokeCount.countDown(); + responseBuffer.append(filterContext.getRequestURI().getHost()) + .append(filterContext.getRequestURI().getPath()) + .append(filterContext.getResponseStatusCode()) + .append(filterContext.getResponseFirstHeader("Content-Type")) + .append(filterContext.getRequestContext().get("key1")) + .append(filterContext.getRequestContext().get("key2")); + return filterContext; + } + }); + Map<String, Object> context = new HashMap<>(); + context.put("key1", "value1"); + context.put("key2", "value2"); + ClientTestDriver driver = ClientTestDriver.newInstance(newFilterModule(filters)); + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/foo.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" + + "Host: .+\r\n" + + "Connection: keep-alive\r\n" + + "Accept: .+/.+\r\n" + + "User-Agent: JDisc/1.0\r\n" + + "\r\n"), + responseChunks("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n"), + expectedResponseStatus(200), + expectedResponseMessage("OK"), + expectedResponseHeaders(), + expectedResponseContent(), + context); + + filterInvokeCount.await(60, TimeUnit.SECONDS); + assertEquals(0, filterInvokeCount.getCount()); + assertEquals("localhost/foo.html200text/plain; charset=UTF-8value1value2", responseBuffer.toString()); + assertTrue(driver.close()); + } + + @Test(enabled = false) + public void requireThatResponseFilterHandlesFilterExceptionProperly() throws Exception { + ResponseFilter filter = new ResponseFilter() { + + @Override + public ResponseFilterContext filter(ResponseFilterContext filterContext) throws FilterException { + throw new FilterException("Request aborted."); + } + }; + ClientTestDriver driver = ClientTestDriver.newInstance(newFilterModule(filter)); + assertRequest(driver.currentContainer(), driver.server(), + requestUri("/foo.html"), + requestHeaders(), + requestContent(), + expectedRequestChunks(), + responseChunks("HTTP/1.1 400 \r\n" + + "\r\n"), + expectedResponseStatus(400), + expectedResponseMessage("Request aborted."), + expectedResponseHeaders(), + expectedResponseContent()); + assertTrue(driver.close()); + } + + private static class MyMetric extends AbstractModule implements MetricConsumer { + + final AtomicInteger numContexts = new AtomicInteger(0); + final AtomicInteger numCalls = new AtomicInteger(0); + final Metric.Context context; + + MyMetric(Metric.Context context) { + this.context = context; + } + + @Override + protected void configure() { + bind(MetricConsumer.class).toInstance(this); + } + + @Override + public void set(String key, Number val, Metric.Context context) { + assertSame(this.context, context); + numCalls.incrementAndGet(); + } + + @Override + public void add(String key, Number val, Metric.Context context) { + assertSame(this.context, context); + numCalls.incrementAndGet(); + } + + @Override + public Metric.Context createContext(Map<String, ?> properties) { + numContexts.incrementAndGet(); + return context; + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java new file mode 100644 index 00000000000..7f5e22e48da --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.ProxyServer; +import org.testng.annotations.Test; + +import java.net.URI; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ProxyServerFactoryTestCase { + + @Test(enabled = false) + public void requireThatProxyServerFactoryWorks() { + assertNull(ProxyServerFactory.newInstance(null)); + + ProxyServer proxy = ProxyServerFactory.newInstance(URI.create("http://localhost:1234")); + assertEquals(ProxyServer.Protocol.HTTP, proxy.getProtocol()); + assertEquals("localhost", proxy.getHost()); + assertEquals(1234, proxy.getPort()); + assertNull(proxy.getPrincipal()); + assertNull(proxy.getPassword()); + + proxy = ProxyServerFactory.newInstance(URI.create("http://foo@localhost:1234")); + assertEquals(ProxyServer.Protocol.HTTP, proxy.getProtocol()); + assertEquals("localhost", proxy.getHost()); + assertEquals(1234, proxy.getPort()); + assertEquals("foo", proxy.getPrincipal()); + assertNull(proxy.getPassword()); + + proxy = ProxyServerFactory.newInstance(URI.create("https://foo:bar@localhost:1234")); + assertEquals(ProxyServer.Protocol.HTTPS, proxy.getProtocol()); + assertEquals("localhost", proxy.getHost()); + assertEquals(1234, proxy.getPort()); + assertEquals("foo", proxy.getPrincipal()); + assertEquals("bar", proxy.getPassword()); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java new file mode 100644 index 00000000000..3f9912fda33 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +public class WebSocketClientRequestTestCase { + + @Test(enabled = false) + public void testWebSocketRequestReturnsCorrectContentChannel() { + AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class); + Request request = Mockito.mock(Request.class); + ResponseHandler respHandler = Mockito.mock(ResponseHandler.class); + Metric metric = Mockito.mock(Metric.class); + Metric.Context ctx = Mockito.mock(Metric.Context.class); + + ContentChannel cc = WebSocketClientRequest.executeRequest(client, request, respHandler, metric, ctx); + assertTrue(cc instanceof WebSocketContent); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java new file mode 100644 index 00000000000..4ab851ac5b9 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java @@ -0,0 +1,97 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.ListenableFuture; +import com.ning.http.client.Request; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketUpgradeHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.testng.AssertJUnit.fail; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +@SuppressWarnings("unchecked") +public class WebSocketContentTestCase { + + private final byte[] TEST_DATA = "test data".getBytes(StandardCharsets.UTF_8); + + @Test(enabled = false) + public void testContentChannelWriteAndClose() throws Exception{ + AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class); + com.yahoo.jdisc.Request request = Mockito.mock(com.yahoo.jdisc.Request.class); + Mockito.when(request.getUri()).thenReturn(new URI("")); + + WebSocket websocket = Mockito.mock(WebSocket.class); + Mockito.when(websocket.isOpen()).thenReturn(true); + ListenableFuture<WebSocket> future = Mockito.mock(ListenableFuture.class); + Mockito.when(client.executeRequest((Request)Mockito.isNotNull(), (WebSocketUpgradeHandler)Mockito.anyObject())) + .thenReturn(future); + Mockito.when(future.get()).thenReturn(websocket); + + WebSocketContent underTest = new WebSocketContent(client, request, Mockito.mock(WebSocketUpgradeHandler.class)); + + CompletionHandler completionHandler = Mockito.mock(CompletionHandler.class); + underTest.write(ByteBuffer.wrap(TEST_DATA),completionHandler); + + Mockito.verify(completionHandler,Mockito.atLeastOnce()).completed(); + + CompletionHandler closeHandler = Mockito.mock(CompletionHandler.class); + underTest.close(closeHandler); + Mockito.verify(closeHandler).completed(); + Mockito.verify(websocket).close(); + Mockito.verify(websocket).sendMessage(TEST_DATA); + } + + @Test(enabled = false) + public void testWritingToAClosedContentChannel() throws Exception{ + AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class); + com.yahoo.jdisc.Request request = Mockito.mock(com.yahoo.jdisc.Request.class); + Mockito.when(request.getUri()).thenReturn(new URI("")); + WebSocket websocket = Mockito.mock(WebSocket.class); + ListenableFuture<WebSocket> future = Mockito.mock(ListenableFuture.class); + Mockito.when(client.executeRequest((Request)Mockito.isNotNull(), (WebSocketUpgradeHandler)Mockito.anyObject())) + .thenReturn(future); + Mockito.when(future.get()).thenReturn(websocket); + + WebSocketContent underTest = new WebSocketContent(client, request, Mockito.mock(WebSocketUpgradeHandler.class)); + underTest.close(Mockito.mock(CompletionHandler.class)); + + // opens a new websocket + underTest.write(ByteBuffer.wrap(TEST_DATA), Mockito.mock(CompletionHandler.class)); + } + + @Test(enabled = false) + public void testExceptionalPathInExecuteRequest() throws Exception{ + AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class); + com.yahoo.jdisc.Request request = Mockito.mock(com.yahoo.jdisc.Request.class); + Mockito.when(request.getUri()).thenReturn(new URI("")); + + WebSocket websocket = Mockito.mock(WebSocket.class); + Mockito.when(websocket.isOpen()).thenReturn(true); + ListenableFuture<WebSocket> future = Mockito.mock(ListenableFuture.class); + Mockito.when(client.executeRequest((Request)Mockito.isNotNull(), (WebSocketUpgradeHandler)Mockito.anyObject())) + .thenReturn(future); + Mockito.when(future.get()).thenReturn(websocket); + Mockito.when(websocket.sendMessage((byte[])Mockito.any())).thenThrow(new RuntimeException()); + + WebSocketContent underTest = new WebSocketContent(client, request, Mockito.mock(WebSocketUpgradeHandler.class)); + + CompletionHandler completionHandler = Mockito.mock(CompletionHandler.class); + + try { + underTest.write(ByteBuffer.wrap(TEST_DATA),completionHandler); + fail(); + } catch(RuntimeException e) { + // Expected + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java new file mode 100644 index 00000000000..68283625579 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java @@ -0,0 +1,148 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client; + +import com.ning.http.client.websocket.WebSocket; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.FutureResponse; +import com.yahoo.jdisc.handler.ReadableContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpResponse; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.net.ConnectException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +public class WebSocketHandlerTestCase { + + @Test(enabled = false) + public void requireThatOnOpenDoesNothing() { + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + newSocketHandler(responseHandler).onOpen(Mockito.mock(WebSocket.class)); + Mockito.verifyZeroInteractions(responseHandler); + } + + @Test(enabled = false) + public void requireThatOnFragmentDoesNothing() { + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + newSocketHandler(responseHandler).onFragment(new byte[] { 6, 9 }, false); + Mockito.verifyZeroInteractions(responseHandler); + } + + @Test(enabled = false) + public void requireThatOnLastFragmentDoesNothing() { + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + newSocketHandler(responseHandler).onFragment(new byte[] { 6, 9 }, true); + Mockito.verifyZeroInteractions(responseHandler); + } + + @Test(enabled = false) + public void requireThatResponseIsDispatchedOnFirstMessage() throws Exception { + ReadableContentChannel content = new ReadableContentChannel(); + FutureResponse responseHandler = new FutureResponse(content); + try { + responseHandler.get(100, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + + } + newSocketHandler(responseHandler).onMessage(new byte[] { 6, 9 }); + Response response = responseHandler.get(60, TimeUnit.SECONDS); + assertTrue(response instanceof HttpResponse); + assertEquals(Response.Status.OK, response.getStatus()); + } + + @Test(enabled = false) + public void requireThatResponseBytesAreWritten() { + ReadableContentChannel content = new ReadableContentChannel(); + newSocketHandler(content).onMessage(new byte[] { 6, 9 }); + ByteBuffer buf = content.read(); + assertEquals(2, buf.remaining()); + assertEquals(6, buf.get()); + assertEquals(9, buf.get()); + } + + @Test(enabled = false) + public void requireThatEmptyResponsesCanBeSent() throws Exception { + ReadableContentChannel content = new ReadableContentChannel(); + FutureResponse responseHandler = new FutureResponse(content); + newSocketHandler(responseHandler).onClose(Mockito.mock(WebSocket.class)); + assertResponse(responseHandler, Response.Status.OK); + assertNull(content.read()); + } + + @Test(enabled = false) + public void requireThatEarlyErrorRespondsWithError() throws Exception { + assertErrorResponse(new ConnectException(), Response.Status.SERVICE_UNAVAILABLE); + assertErrorResponse(new TimeoutException(), Response.Status.REQUEST_TIMEOUT); + assertErrorResponse(new Throwable(), Response.Status.BAD_REQUEST); + } + + @Test(enabled = false) + public void requireThatWriteCompletionFailureClosesResponseContent() { + CloseableContentChannel content = new CloseableContentChannel(); + FutureResponse responseHandler = new FutureResponse(content); + WebSocketHandler socketHandler = newSocketHandler(responseHandler); + socketHandler.onMessage(new byte[] { 6, 9 }); + assertFalse(content.closed); + content.handler.failed(new Throwable()); + assertTrue(content.closed); + } + + private static void assertErrorResponse(Throwable t, int expectedStatus) throws Exception { + ReadableContentChannel content = new ReadableContentChannel(); + FutureResponse responseHandler = new FutureResponse(content); + newSocketHandler(responseHandler).onError(t); + assertResponse(responseHandler, expectedStatus); + assertNull(content.read()); + } + + private static void assertResponse(FutureResponse responseHandler, int expectedStatus) throws Exception { + Response response = responseHandler.get(60, TimeUnit.SECONDS); + assertTrue(response instanceof HttpResponse); + assertEquals(expectedStatus, response.getStatus()); + } + + private static WebSocketHandler newSocketHandler(ResponseHandler responseHandler) { + return new WebSocketHandler(Mockito.mock(Request.class), responseHandler, Mockito.mock(Metric.class), + Mockito.mock(Metric.Context.class)); + } + + private static WebSocketHandler newSocketHandler(ContentChannel responseContent) { + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + Mockito.when(responseHandler.handleResponse(Mockito.any(Response.class))).thenReturn(responseContent); + return new WebSocketHandler(Mockito.mock(Request.class), responseHandler, Mockito.mock(Metric.class), + Mockito.mock(Metric.Context.class)); + } + + private static class CloseableContentChannel implements ContentChannel { + + CompletionHandler handler; + boolean closed = false; + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + this.handler = handler; + } + + @Override + public void close(CompletionHandler handler) { + closed = true; + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java new file mode 100644 index 00000000000..b2154349549 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.client.filter.core; + +import com.ning.http.client.FluentCaseInsensitiveStringsMap; +import com.ning.http.client.HttpResponseHeaders; +import com.ning.http.client.HttpResponseStatus; +import com.ning.http.client.Request; +import com.ning.http.client.filter.FilterContext; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.client.filter.ResponseFilterContext; +import org.testng.annotations.Test; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; + +/** + * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a> + */ +public class ResponseFilterBridgeTestCase { + + @Test(enabled = false) + public void requireThatResponseFilterBridgeConvertsFieldsProperly() throws MalformedURLException, URISyntaxException { + ResponseFilterContext responseFilterContext = ResponseFilterBridge.toResponseFilterContext( + constructFilterContext(), + constructRequest() + ); + + assertEquals("http://localhost:8080/echo", responseFilterContext.getRequestURI().toString()); + assertEquals(200, responseFilterContext.getResponseStatusCode()); + assertEquals("v1", responseFilterContext.getResponseFirstHeader("k1")); + assertEquals("v2", responseFilterContext.getResponseFirstHeader("k2")); + Map<String, Object> customParams = responseFilterContext.getRequestContext(); + assertEquals("cv1", customParams.get("c1")); + assertEquals("cv2", customParams.get("c2")); + + } + + private HttpRequest constructRequest() { + HttpRequest request = mock(HttpRequest.class); + Map<String, Object> customParams = new HashMap<>(); + customParams.put("c1", "cv1"); + customParams.put("c2", "cv2"); + when(request.context()).thenReturn(customParams); + return request; + } + + private FilterContext<?> constructFilterContext() throws MalformedURLException, URISyntaxException { + FilterContext.FilterContextBuilder<?> builder = new FilterContext.FilterContextBuilder<>(); + + Request request = mock(Request.class); + URL url = new URL("http://localhost:8080/echo"); + URI reqURI = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), + url.getQuery(), url.getRef()); + when(request.getURI()).thenReturn(reqURI); + + HttpResponseStatus responseStatus = mock(HttpResponseStatus.class); + when(responseStatus.getStatusCode()).thenReturn(200); + + HttpResponseHeaders responseHeaders = mock(HttpResponseHeaders.class); + FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap(); + headers.add("k1", "v1", "v12", "v13"); + headers.add("k2", "v2"); + when(responseHeaders.getHeaders()).thenReturn(headers); + + builder.request(request); + builder.responseStatus(responseStatus); + builder.responseHeaders(responseHeaders); + + return builder.build(); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java new file mode 100644 index 00000000000..149ddbc962c --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java @@ -0,0 +1,357 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import static org.testng.AssertJUnit.assertTrue; + +import java.net.InetSocketAddress; +import java.net.URI; + +import java.util.*; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.test.TestDriver; + +import com.yahoo.jdisc.http.Cookie; + +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Version; + +public class DiscFilterRequestTest { + + private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { + InetSocketAddress address = new InetSocketAddress("example.yahoo.com", 69); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); + request.release(); + assertTrue(driver.close()); + return request; + } + + @Test + public void testRequestConstruction(){ + URI uri = URI.create("http://localhost:8080/test?param1=abc"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); + httpReq.headers().add("X-Custom-Header", "custom_header"); + List<Cookie> cookies = new ArrayList<Cookie>(); + cookies.add(new Cookie("XYZ", "value")); + cookies.add(new Cookie("ABC", "value")); + httpReq.encodeCookieHeader(cookies); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertSame(request.getParentRequest(),httpReq); + Assert.assertEquals(request.getHeader("X-Custom-Header"),"custom_header"); + Assert.assertEquals(request.getHeader(HttpHeaders.Names.CONTENT_TYPE),"text/html;charset=UTF-8"); + + List<Cookie> c = request.getCookies(); + Assert.assertNotNull(c); + Assert.assertEquals(c.size(), 2); + + Assert.assertEquals(request.getParameter("param1"),"abc"); + Assert.assertNull(request.getParameter("param2")); + Assert.assertEquals(request.getVersion(),Version.HTTP_1_1); + Assert.assertEquals(request.getProtocol(),Version.HTTP_1_1.name()); + Assert.assertNull(request.getRequestedSessionId()); + } + + @Test + public void testRequestConstruction2() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add("some-header", "some-value"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + request.addHeader("some-header", "some-value"); + String value = request.getUntreatedHeaders().get("some-header").get(0); + Assert.assertEquals(value,"some-value"); + } + + @Test + public void testRequestAttributes() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setAttribute("some_attr", "some_value"); + + Assert.assertEquals(request.containsAttribute("some_attr"),true); + + Assert.assertEquals(request.getAttribute("some_attr"),"some_value"); + + } + + @Test + public void testGetAttributeNames() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setAttribute("some_attr_1", "some_value1"); + request.setAttribute("some_attr_2", "some_value2"); + + Enumeration<String> e = request.getAttributeNames(); + List<String> attrList = Collections.list(e); + Assert.assertEquals(2, attrList.size()); + Assert.assertEquals(attrList.contains("some_attr_1"), true); + Assert.assertEquals(attrList.contains("some_attr_2"), true); + + } + + @Test + public void testRemoveAttribute() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setAttribute("some_attr", "some_value"); + + Assert.assertEquals(request.containsAttribute("some_attr"),true); + + request.removeAttribute("some_attr"); + + Assert.assertEquals(request.containsAttribute("some_attr"),false); + } + + @Test + public void testGetIntHeader() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(-1, request.getIntHeader("int_header")); + + request.addHeader("int_header", String.valueOf(5)); + + Assert.assertEquals(5, request.getIntHeader("int_header")); + } + + @Test + public void testDateHeader() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + + Assert.assertEquals(-1, request.getDateHeader(HttpHeaders.Names.IF_MODIFIED_SINCE)); + + request.addHeader(HttpHeaders.Names.IF_MODIFIED_SINCE, "Sat, 29 Oct 1994 19:43:31 GMT"); + + Assert.assertEquals(783459811000L, request.getDateHeader(HttpHeaders.Names.IF_MODIFIED_SINCE)); + } + + @Test + public void testParameterAPIsAsList() { + URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc¶m2=xyz¶m2=pqr"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.getParameter("param1"),"abc"); + + List<String> values = request.getParameterValuesAsList("param2"); + Assert.assertEquals(values.get(0),"xyz"); + Assert.assertEquals(values.get(1),"pqr"); + + List<String> paramNames = request.getParameterNamesAsList(); + Assert.assertEquals(paramNames.size(), 2); + + } + + @Test + public void testParameterAPI(){ + URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc¶m2=xyz¶m2=pqr"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.getParameter("param1"),"abc"); + + Enumeration<String> values = request.getParameterValues("param2"); + List<String> valuesList = Collections.list(values); + Assert.assertEquals(valuesList.get(0),"xyz"); + Assert.assertEquals(valuesList.get(1),"pqr"); + + Enumeration<String> paramNames = request.getParameterNames(); + List<String> paramNamesList = Collections.list(paramNames); + Assert.assertEquals(paramNamesList.size(), 2); + } + + @Test + public void testGetHeaderNamesAsList() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "multipart/form-data"); + httpReq.headers().add("header_1", "value1"); + httpReq.headers().add("header_2", "value2"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(request.getHeaderNamesAsList() instanceof List, true); + Assert.assertEquals(request.getHeaderNamesAsList().size(), 3); + } + + @Test + public void testGetHeadersAsList() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(request.getHeaderNamesAsList() instanceof List, true); + Assert.assertEquals(request.getHeaderNamesAsList().size(), 0); + + httpReq.headers().add("header_1", "value1"); + httpReq.headers().add("header_1", "value2"); + + Assert.assertEquals(request.getHeadersAsList("header_1").size(), 2); + } + + @Test + public void testIsMultipart() { + + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "multipart/form-data"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(true,DiscFilterRequest.isMultipart(request)); + + httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); + request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(DiscFilterRequest.isMultipart(request),false); + + Assert.assertEquals(DiscFilterRequest.isMultipart(null),false); + + + httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(DiscFilterRequest.isMultipart(request),false); + } + + @Test + public void testGetRemotePortLocalPort() { + + URI uri = URI.create("http://example.yahoo.com:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(69, request.getRemotePort()); + Assert.assertEquals(8080, request.getLocalPort()); + + if (request.getRemoteHost() != null) // if we have network + Assert.assertEquals("example.yahoo.com", request.getRemoteHost()); + + request.setRemoteAddr("1.1.1.1"); + + Assert.assertEquals("1.1.1.1",request.getRemoteAddr()); + } + + @Test + public void testCharacterEncoding() throws Exception { + URI uri = URI.create("http://example.yahoo.com:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setHeaders(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); + + Assert.assertEquals(request.getCharacterEncoding(), "UTF-8"); + + httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + request = new JdiscFilterRequest(httpReq); + request.setHeaders(HttpHeaders.Names.CONTENT_TYPE, "text/html"); + request.setCharacterEncoding("UTF-8"); + + Assert.assertEquals(request.getCharacterEncoding(),"UTF-8"); + + Assert.assertEquals(request.getHeader(HttpHeaders.Names.CONTENT_TYPE),"text/html;charset=UTF-8"); + } + + @Test + public void testSetScheme() throws Exception { + URI uri = URI.create("https://example.yahoo.com:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + request.setScheme("http", true); + System.out.println(request.getUri().toString()); + Assert.assertEquals(request.getUri().toString(), "http://example.yahoo.com:8080/test"); + } + + @Test + public void testGetServerPort() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.getServerPort(), 80); + + request.setUri(URI.create("https://example.yahoo.com/test")); + Assert.assertEquals(request.getServerPort(), 443); + + } + + @Test + public void testIsSecure() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.isSecure(), false); + + request.setUri(URI.create("https://example.yahoo.com/test")); + Assert.assertEquals(request.isSecure(), true); + + } + + @Test + public void requireThatUnresolvableRemoteAddressesAreSupported() { + URI uri = URI.create("http://doesnotresolve.zzz:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertNull(request.getLocalAddr()); + } + + @Test + public void testGetUntreatedHeaders() { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add("key1", "value1"); + httpReq.headers().add("key2", Arrays.asList("value1","value2")); + + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + HeaderFields headers = request.getUntreatedHeaders(); + Assert.assertEquals(headers.keySet().size(), 2); + Assert.assertEquals(headers.get("key1").get(0), "value1" ); + Assert.assertEquals(headers.get("key2").get(0), "value1" ); + Assert.assertEquals(headers.get("key2").get(1), "value2" ); + } + + @Test + public void testClearCookies() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().put(HttpHeaders.Names.COOKIE, "XYZ=value"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.clearCookies(); + Assert.assertNull(request.getHeader(HttpHeaders.Names.COOKIE)); + } + + @Test + public void testGetWrapedCookies() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().put(HttpHeaders.Names.COOKIE, "XYZ=value"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + JDiscCookieWrapper[] wrappers = request.getWrappedCookies(); + Assert.assertEquals(wrappers.length ,1); + Assert.assertEquals(wrappers[0].getName(), "XYZ"); + Assert.assertEquals(wrappers[0].getValue(), "value"); + } + + @Test + public void testAddCookie() { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.addCookie(JDiscCookieWrapper.wrap(new Cookie("name", "value"))); + + List<Cookie> cookies = request.getCookies(); + Assert.assertEquals(cookies.size(), 1); + Assert.assertEquals(cookies.get(0).getName(), "name"); + Assert.assertEquals(cookies.get(0).getValue(), "value"); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java new file mode 100644 index 00000000000..f52d4be8c84 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java @@ -0,0 +1,115 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import static org.testng.AssertJUnit.assertTrue; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Collections; + +import java.util.List; + +import com.yahoo.jdisc.Request; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.test.TestDriver; + +public class DiscFilterResponseTest { + + private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { + InetSocketAddress address = new InetSocketAddress("java.corp.yahoo.com", 69); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); + request.release(); + assertTrue(driver.close()); + return request; + } + + public static HttpResponse newResponse(Request request, int status) { + return HttpResponse.newInstance(status); + } + + @Test + public void testGetSetStatus() { + HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), + HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); + + Assert.assertEquals(response.getStatus(), HttpResponse.Status.OK); + response.setStatus(HttpResponse.Status.REQUEST_TIMEOUT); + Assert.assertEquals(response.getStatus(), HttpResponse.Status.REQUEST_TIMEOUT); + } + + @Test + public void testAttributes() { + HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), + HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); + response.setAttribute("attr_1", "value1"); + Assert.assertEquals(response.getAttribute("attr_1"), "value1"); + List<String> list = Collections.list(response.getAttributeNames()); + Assert.assertEquals(list.get(0), "attr_1"); + response.removeAttribute("attr_1"); + Assert.assertNull(response.getAttribute("attr_1")); + } + + @Test + public void testAddHeader() { + HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), + HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); + response.addHeader("header1", "value1"); + Assert.assertEquals(response.getHeader("header1"), "value1"); + } + + @Test + public void testAddCookie() { + URI uri = URI.create("http://example.corp.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + response.addCookie(JDiscCookieWrapper.wrap(new Cookie("name", "value"))); + + List<Cookie> cookies = response.getCookies(); + Assert.assertEquals(cookies.size(),1); + Assert.assertEquals(cookies.get(0).getName(),"name"); + } + + @Test + public void testSetCookie() { + URI uri = URI.create("http://example.corp.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + response.setCookie("name", "value"); + List<Cookie> cookies = response.getCookies(); + Assert.assertEquals(cookies.size(),1); + Assert.assertEquals(cookies.get(0).getName(),"name"); + + } + + @Test + public void testSetHeader() { + URI uri = URI.create("http://example.corp.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + response.setHeader("name", "value"); + Assert.assertEquals(response.getHeader("name"), "value"); + } + + @Test + public void testGetParentResponse() { + URI uri = URI.create("http://example.corp.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + Assert.assertSame(response.getParentResponse(), httpResp); + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java new file mode 100644 index 00000000000..9086582ccae --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.chain.EmptyRequestFilter; +import com.yahoo.jdisc.service.CurrentContainer; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.jdisc.http.HttpRequest.Version; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class EmptyRequestFilterTestCase { + + @Test + public void requireThatEmptyFilterDoesNothing() throws Exception { + final HttpRequest lhs = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); + final HttpRequest rhs = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); + + EmptyRequestFilter.INSTANCE.filter(rhs, mock(ResponseHandler.class)); + + assertEquals(lhs.headers(), rhs.headers()); + assertEquals(lhs.context(), rhs.context()); + assertEquals(lhs.getTimeout(TimeUnit.MILLISECONDS), rhs.getTimeout(TimeUnit.MILLISECONDS)); + assertEquals(lhs.parameters(), rhs.parameters()); + assertEquals(lhs.getMethod(), rhs.getMethod()); + assertEquals(lhs.getVersion(), rhs.getVersion()); + assertEquals(lhs.getRemoteAddress(), rhs.getRemoteAddress()); + } + + private static HttpRequest newRequest( + final Method method, final String uri, final Version version) { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return HttpRequest.newServerRequest(currentContainer, URI.create(uri), method, version); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java new file mode 100644 index 00000000000..1294e8e6b98 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.filter.chain.EmptyResponseFilter; +import com.yahoo.jdisc.service.CurrentContainer; +import org.testng.annotations.Test; + +import java.net.URI; + +import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.jdisc.http.HttpRequest.Version; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class EmptyResponseFilterTestCase { + + @Test + public void requireThatEmptyFilterDoesNothing() throws Exception { + final HttpRequest request = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); + final HttpResponse lhs = HttpResponse.newInstance(Response.Status.OK); + final HttpResponse rhs = HttpResponse.newInstance(Response.Status.OK); + + EmptyResponseFilter.INSTANCE.filter(lhs, null); + + assertEquals(lhs.headers(), rhs.headers()); + assertEquals(lhs.context(), rhs.context()); + assertEquals(lhs.getError(), rhs.getError()); + assertEquals(lhs.getMessage(), rhs.getMessage()); + } + + private static HttpRequest newRequest(final Method method, final String uri, final Version version) { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return HttpRequest.newServerRequest(currentContainer, URI.create(uri), method, version); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java new file mode 100644 index 00000000000..4d0bfa8e334 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import java.util.concurrent.TimeUnit; + +import com.yahoo.jdisc.http.filter.JDiscCookieWrapper; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.yahoo.jdisc.http.Cookie; + +public class JDiscCookieWrapperTest { + + @Test + public void requireThatWrapWorks() { + Cookie cookie = new Cookie("name", "value"); + JDiscCookieWrapper wrapper = JDiscCookieWrapper.wrap(cookie); + + wrapper.setComment("comment"); + wrapper.setDomain("yahoo.com"); + wrapper.setMaxAge(10); + wrapper.setPath("/path"); + wrapper.setVersion(1); + + Assert.assertEquals(wrapper.getName(), cookie.getName()); + Assert.assertEquals(wrapper.getValue(), cookie.getValue()); + Assert.assertEquals(wrapper.getDomain(), cookie.getDomain()); + Assert.assertEquals(wrapper.getComment(), cookie.getComment()); + Assert.assertEquals(wrapper.getMaxAge(), cookie.getMaxAge(TimeUnit.SECONDS)); + Assert.assertEquals(wrapper.getPath(), cookie.getPath()); + Assert.assertEquals(wrapper.getVersion(), cookie.getVersion()); + Assert.assertEquals(wrapper.getSecure(), cookie.isSecure()); + + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java new file mode 100644 index 00000000000..181298ea0e3 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ResponseHeaderFilter extends AbstractResource implements ResponseFilter { + + private final String key; + private final String val; + + public ResponseHeaderFilter(String key, String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(Response response, Request request) { + response.headers().add(key, val); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java new file mode 100644 index 00000000000..e0c949afe54 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java @@ -0,0 +1,173 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.yahoo.jdisc.http.HttpRequest.Version; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Test the parts of the DiscFilterRequest API that are implemented + * by ServletFilterRequest, both directly and indirectly via + * {@link com.yahoo.jdisc.http.servlet.ServletRequest}. + * + * @author gjoranv + * @since 5.27 + */ +public class ServletFilterRequestTest { + + private final String host = "host1"; + private final int port = 8080; + private final String path = "/path1"; + private final String paramName = "param1"; + private final String paramValue = "p1"; + private final String listParamName = "listParam"; + private final String[] listParamValue = new String[]{"1", "2"}; + private final String headerName = "header1"; + private final String headerValue = "h1"; + private final String attributeName = "attribute1"; + private final String attributeValue = "a1"; + + private URI uri; + private DiscFilterRequest filterRequest; + private ServletRequest parentRequest; + + @BeforeMethod + private void init() throws Exception { + uri = new URI("http", null, host, port, path, paramName + "=" + paramValue, null); + + filterRequest = new ServletFilterRequest(newServletRequest()); + parentRequest = ((ServletFilterRequest)filterRequest).getServletRequest(); + } + + private ServletRequest newServletRequest() throws Exception { + MockHttpServletRequest parent = new MockHttpServletRequest("GET", uri.toString()); + parent.setProtocol(Version.HTTP_1_1.toString()); + parent.setRemoteHost(host); + parent.setRemotePort(port); + parent.setParameter(paramName, paramValue); + parent.setParameter(listParamName, listParamValue); + parent.addHeader(headerName, headerValue); + parent.setAttribute(attributeName, attributeValue); + return new ServletRequest(parent, uri); + } + + @Test + public void parent_properties_are_propagated_to_disc_filter_request() throws Exception { + assertEquals(filterRequest.getVersion(), Version.HTTP_1_1); + assertEquals(filterRequest.getMethod(), "GET"); + assertEquals(filterRequest.getUri(), uri); + assertEquals(filterRequest.getRemoteHost(), host); + assertEquals(filterRequest.getRemotePort(), port); + assertEquals(filterRequest.getRequestURI(), path); // getRequestUri return only the path by design + + assertEquals(filterRequest.getParameter(paramName), paramValue); + assertEquals(filterRequest.getParameterMap().get(paramName), + Collections.singletonList(paramValue)); + assertEquals(filterRequest.getParameterValuesAsList(listParamName), Arrays.asList(listParamValue)); + + assertEquals(filterRequest.getHeader(headerName), headerValue); + assertEquals(filterRequest.getAttribute(attributeName), attributeValue); + } + + @Test + public void untreatedHeaders_is_populated_from_the_parent_request() { + assertEquals(filterRequest.getUntreatedHeaders().getFirst(headerName), headerValue); + } + + @Test + public void uri_can_be_set() throws Exception { + URI newUri = new URI("http", null, host, port + 1, path, paramName + "=" + paramValue, null); + filterRequest.setUri(newUri); + + assertEquals(filterRequest.getUri(), newUri); + assertEquals(parentRequest.getUri(), newUri); + } + + @Test + public void attributes_can_be_set() throws Exception { + String name = "newAttribute"; + String value = name + "Value"; + filterRequest.setAttribute(name, value); + + assertEquals(filterRequest.getAttribute(name), value); + assertEquals(parentRequest.getAttribute(name), value); + } + + @Test + public void attributes_can_be_removed() { + filterRequest.removeAttribute(attributeName); + + assertEquals(filterRequest.getAttribute(attributeName), null); + assertEquals(parentRequest.getAttribute(attributeName), null); + } + + @Test + public void headers_can_be_set() throws Exception { + String name = "myHeader"; + String value = name + "Value"; + filterRequest.setHeaders(name, value); + + assertEquals(filterRequest.getHeader(name), value); + assertEquals(parentRequest.getHeader(name), value); + } + + @Test + public void headers_can_be_removed() throws Exception { + filterRequest.removeHeaders(headerName); + + assertEquals(filterRequest.getHeader(headerName), null); + assertEquals(parentRequest.getHeader(headerName), null); + } + + @Test + public void headers_can_be_added() { + String value = "h2"; + filterRequest.addHeader(headerName, value); + + List<String> expected = Arrays.asList(headerValue, value); + assertEquals(filterRequest.getHeadersAsList(headerName), expected); + assertEquals(Collections.list(parentRequest.getHeaders(headerName)), expected); + } + + @Test + public void cookies_can_be_added_and_removed() { + Cookie cookie = new Cookie("name", "value"); + filterRequest.addCookie(JDiscCookieWrapper.wrap(cookie)); + + assertEquals(filterRequest.getCookies(), Collections.singletonList(cookie)); + assertEquals(parentRequest.getCookies().length, 1); + + javax.servlet.http.Cookie servletCookie = parentRequest.getCookies()[0]; + assertEquals(servletCookie.getName(), cookie.getName()); + assertEquals(servletCookie.getValue(), cookie.getValue()); + + filterRequest.clearCookies(); + assertTrue(filterRequest.getCookies().isEmpty()); + assertEquals(parentRequest.getCookies().length, 0); + } + + @Test + public void character_encoding_can_be_set() throws Exception { + // ContentType must be non-null before setting character encoding + filterRequest.setHeaders(HttpHeaders.Names.CONTENT_TYPE, ""); + + String encoding = "myEncoding"; + filterRequest.setCharacterEncoding(encoding); + + assertTrue(filterRequest.getCharacterEncoding().contains(encoding)); + assertTrue(parentRequest.getCharacterEncoding().contains(encoding)); + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java new file mode 100644 index 00000000000..dbbef448f18 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletResponse; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Arrays; + +import static org.testng.Assert.assertEquals; + +/** + * @author gjoranv + * @since 5.27 + */ +public class ServletFilterResponseTest { + + private final String headerName = "header1"; + private final String headerValue = "h1"; + + private DiscFilterResponse filterResponse; + private ServletResponse parentResponse; + + @BeforeMethod + private void init() throws Exception { + filterResponse = new ServletFilterResponse(newServletResponse()); + parentResponse = ((ServletFilterResponse)filterResponse).getServletResponse(); + + } + + private ServletResponse newServletResponse() throws Exception { + MockServletResponse parent = new MockServletResponse(); + parent.addHeader(headerName, headerValue); + return new ServletResponse(parent); + } + + + @Test + public void headers_can_be_set() throws Exception { + String name = "myHeader"; + String value = name + "Value"; + filterResponse.setHeaders(name, value); + + assertEquals(filterResponse.getHeader(name), value); + assertEquals(parentResponse.getHeader(name), value); + } + + @Test + public void headers_can_be_added() throws Exception { + String newValue = "h2"; + filterResponse.addHeader(headerName, newValue); + + // The DiscFilterResponse has no getHeaders() + assertEquals(filterResponse.getHeader(headerName), newValue); + + assertEquals(parentResponse.getHeaders(headerName), Arrays.asList(headerValue, newValue)); + } + + @Test + public void headers_can_be_removed() throws Exception { + filterResponse.removeHeaders(headerName); + + assertEquals(filterResponse.getHeader(headerName), null); + assertEquals(parentResponse.getHeader(headerName), null); + } + + @Test + public void set_cookie_overwrites_old_values() { + Cookie to_be_removed = new Cookie("to-be-removed", ""); + Cookie to_keep = new Cookie("to-keep", ""); + filterResponse.setCookie(to_be_removed.getName(), to_be_removed.getValue()); + filterResponse.setCookie(to_keep.getName(), to_keep.getValue()); + + assertEquals(filterResponse.getCookies(), Arrays.asList(to_keep)); + assertEquals(parentResponse.getHeaders(HttpHeaders.Names.SET_COOKIE), Arrays.asList(to_keep.toString())); + } + + + private static class MockServletResponse extends org.eclipse.jetty.server.Response { + private MockServletResponse() { + super(null, null); + } + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java new file mode 100644 index 00000000000..3c3b541f986 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.guiceModules; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ConnectorConfig.Builder; +import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.jdisc.http.server.jetty.ConnectorFactory; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import com.yahoo.jdisc.http.ssl.ReaderForPath; +import com.yahoo.jdisc.http.ssl.SslKeyStore; +import com.yahoo.jdisc.http.ssl.SslKeyStoreFactory; + +/** + * Guice module for test ConnectorFactories + * + * @author tonytv + */ +public class ConnectorFactoryRegistryModule implements Module { + + private final Builder connectorConfigBuilder; + + public ConnectorFactoryRegistryModule(Builder connectorConfigBuilder) { + this.connectorConfigBuilder = connectorConfigBuilder; + } + + public ConnectorFactoryRegistryModule() { + this(new Builder()); + } + + @Provides + public ComponentRegistry<ConnectorFactory> connectorFactoryComponentRegistry() { + ComponentRegistry<ConnectorFactory> registry = new ComponentRegistry<>(); + registry.register(ComponentId.createAnonymousComponentId("connector-factory"), + new StaticKeyDbConnectorFactory(new ConnectorConfig(connectorConfigBuilder))); + + registry.freeze(); + return registry; + } + + @Override + public void configure(Binder binder) { + } + + private static class StaticKeyDbConnectorFactory extends ConnectorFactory { + + public StaticKeyDbConnectorFactory(ConnectorConfig connectorConfig) { + super(connectorConfig, new ThrowingSslKeyStoreFactory(), new MockSecretStore()); + } + + } + + private static final class ThrowingSslKeyStoreFactory implements SslKeyStoreFactory { + + @Override + public SslKeyStore createKeyStore(ReaderForPath certificateFile, ReaderForPath keyFile) { + throw new UnsupportedOperationException("A SSL key store factory component is not available"); + } + + @Override + public SslKeyStore createTrustStore(ReaderForPath certificateFile) { + throw new UnsupportedOperationException("A SSL key store factory component is not available"); + } + + } + + private static final class MockSecretStore implements SecretStore { + + @Override + public String getSecret(String key) { + return TestDrivers.KEY_STORE_PASSWORD; + } + + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java new file mode 100644 index 00000000000..cb09d7cf9f8 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.guiceModules; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.yahoo.component.provider.ComponentRegistry; + +import org.eclipse.jetty.servlet.ServletHolder; + +/** + * @author tonytv + */ +public class ServletModule implements Module { + @Override + public void configure(Binder binder) { + } + + @Provides + public ComponentRegistry<ServletHolder> servletHolderComponentRegistry() { + return new ComponentRegistry<>(); + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java new file mode 100644 index 00000000000..7509598cbe2 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; + +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class AccessLogRequestLogTest { + @Test + public void requireThatQueryWithUnquotedSpecialCharactersIsHandled() { + final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + when(httpServletRequest.getRequestURI()).thenReturn("/search/"); + when(httpServletRequest.getQueryString()).thenReturn("query=year:>2010"); + final AccessLogEntry accessLogEntry = new AccessLogEntry(); + + AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(httpServletRequest, accessLogEntry); + + assertThat(accessLogEntry.getURI(), is(not(nullValue()))); + } + + @Test + public void requireThatDoubleQuotingIsNotPerformed() { + final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + final String path = "/search/"; + when(httpServletRequest.getRequestURI()).thenReturn(path); + final String query = "query=year%252010+%3B&customParameter=something"; + when(httpServletRequest.getQueryString()).thenReturn(query); + final AccessLogEntry accessLogEntry = new AccessLogEntry(); + + AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(httpServletRequest, accessLogEntry); + + assertThat(accessLogEntry.getURI().toString(), is(path + '?' + query)); + + } + + @Test + public void requireThatNoQueryPartIsHandledWhenRequestIsMalformed() { + final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + final String path = "/s>earch/"; + when(httpServletRequest.getRequestURI()).thenReturn(path); + final String query = null; + when(httpServletRequest.getQueryString()).thenReturn(query); + final AccessLogEntry accessLogEntry = new AccessLogEntry(); + + AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(httpServletRequest, accessLogEntry); + + assertThat(accessLogEntry.getURI().toString(), is("/s%3Eearch/")); + + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java new file mode 100644 index 00000000000..0e666e826ae --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java @@ -0,0 +1,136 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import org.testng.annotations.Test; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class AlternativeTest { + private static final String MAN = "man"; + private static final String BEAR = "bear"; + private static final String PIG = "pig"; + + @Test + public void singleValue() { + assertThat( + Alternative.preferred(MAN) + .orElseGet(() -> BEAR), + is(MAN)); + } + + @Test + public void singleNull() { + assertThat( + Alternative.preferred(null) + .orElseGet(() -> BEAR), + is(BEAR)); + } + + @Test + public void twoValues() { + assertThat( + Alternative.preferred(MAN) + .alternatively(() -> BEAR) + .orElseGet(() -> PIG), + is(MAN)); + } + + @Test + public void oneNullOneValue() { + assertThat( + Alternative.preferred(null) + .alternatively(() -> MAN) + .orElseGet(() -> BEAR), + is(MAN)); + } + + @Test + public void twoNulls() { + assertThat( + Alternative.preferred(null) + .alternatively(() -> null) + .orElseGet(() -> MAN), + is(MAN)); + } + + @Test + public void singleNullLastResortIsNull() { + assertThat( + Alternative.preferred(null) + .orElseGet(() -> null), + is(nullValue())); + } + + @Test + public void twoNullsLastResortIsNull() { + assertThat( + Alternative.preferred(null) + .alternatively(() -> null) + .orElseGet(() -> null), + is(nullValue())); + } + + @Test + public void oneNullTwoValues() { + assertThat( + Alternative.preferred(null) + .alternatively(() -> MAN) + .alternatively(() -> BEAR) + .orElseGet(() -> PIG), + is(MAN)); + } + + @Test + public void equalValuesMakeEqualAlternatives() { + assertThat(Alternative.preferred(MAN), is(equalTo(Alternative.preferred(MAN)))); + assertThat(Alternative.preferred(BEAR), is(equalTo(Alternative.preferred(BEAR)))); + assertThat(Alternative.preferred(PIG), is(equalTo(Alternative.preferred(PIG)))); + assertThat(Alternative.preferred(null), is(equalTo(Alternative.preferred(null)))); + } + + @Test + public void equalValuesMakeEqualHashCodes() { + assertThat(Alternative.preferred(MAN).hashCode(), is(equalTo(Alternative.preferred(MAN).hashCode()))); + assertThat(Alternative.preferred(BEAR).hashCode(), is(equalTo(Alternative.preferred(BEAR).hashCode()))); + assertThat(Alternative.preferred(PIG).hashCode(), is(equalTo(Alternative.preferred(PIG).hashCode()))); + assertThat(Alternative.preferred(null).hashCode(), is(equalTo(Alternative.preferred(null).hashCode()))); + } + + @Test + public void unequalValuesMakeUnequalAlternatives() { + assertThat(Alternative.preferred(MAN), is(not(equalTo(Alternative.preferred(BEAR))))); + assertThat(Alternative.preferred(MAN), is(not(equalTo(Alternative.preferred(PIG))))); + assertThat(Alternative.preferred(MAN), is(not(equalTo(Alternative.preferred(null))))); + assertThat(Alternative.preferred(BEAR), is(not(equalTo(Alternative.preferred(MAN))))); + assertThat(Alternative.preferred(BEAR), is(not(equalTo(Alternative.preferred(PIG))))); + assertThat(Alternative.preferred(BEAR), is(not(equalTo(Alternative.preferred(null))))); + assertThat(Alternative.preferred(PIG), is(not(equalTo(Alternative.preferred(MAN))))); + assertThat(Alternative.preferred(PIG), is(not(equalTo(Alternative.preferred(BEAR))))); + assertThat(Alternative.preferred(PIG), is(not(equalTo(Alternative.preferred(null))))); + assertThat(Alternative.preferred(null), is(not(equalTo(Alternative.preferred(MAN))))); + assertThat(Alternative.preferred(null), is(not(equalTo(Alternative.preferred(BEAR))))); + assertThat(Alternative.preferred(null), is(not(equalTo(Alternative.preferred(PIG))))); + } + + @Test + public void hashValuesAreDecent() { + final String[] animals = { MAN, BEAR, PIG, "squirrel", "aardvark", "porcupine", "sasquatch", null }; + final Set<Integer> hashCodes = Stream.of(animals) + .map(Alternative::preferred) + .map(Alternative::hashCode) + .collect(Collectors.toSet()); + assertThat(hashCodes.size(), is(greaterThan(animals.length / 2))); // A modest requirement. + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java new file mode 100644 index 00000000000..425c0444252 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java @@ -0,0 +1,165 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.CertificateStore; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.SecretStore; +import com.yahoo.jdisc.http.ssl.ReaderForPath; +import com.yahoo.jdisc.http.ssl.SslKeyStore; +import com.yahoo.jdisc.http.ssl.SslKeyStoreFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.testng.annotations.Test; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.ServerSocketChannel; +import java.util.Collections; +import java.util.Map; + +import static com.yahoo.jdisc.http.ConnectorConfig.*; +import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.JKS; +import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM; +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class ConnectorFactoryTest { + + @Test(expectedExceptions = IllegalArgumentException.class) + public void ssl_jks_config_is_validated() { + ConnectorConfig config = new ConnectorConfig( + new ConnectorConfig.Builder() + .ssl(new Ssl.Builder() + .enabled(true) + .keyStoreType(JKS) + .pemKeyStore( + new Ssl.PemKeyStore.Builder() + .keyPath("nonEmpty")))); + + ConnectorFactory willThrowException = new ConnectorFactory(config, new ThrowingSslKeyStoreFactory(), + new ThrowingSecretStore()); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void ssl_pem_config_is_validated() { + ConnectorConfig config = new ConnectorConfig( + new ConnectorConfig.Builder() + .ssl(new Ssl.Builder() + .enabled(true) + .keyStoreType(PEM) + .keyStorePath("nonEmpty"))); + + ConnectorFactory willThrowException = new ConnectorFactory(config, new ThrowingSslKeyStoreFactory(), + new ThrowingSecretStore()); + } + + @Test + public void requireThatNoPreBoundChannelWorks() throws Exception { + Server server = new Server(); + try { + ConnectorFactory factory = new ConnectorFactory(new ConnectorConfig(new ConnectorConfig.Builder()), + new ThrowingSslKeyStoreFactory(), + new ThrowingSecretStore()); + ConnectorFactory.JDiscServerConnector connector = + (ConnectorFactory.JDiscServerConnector)factory.createConnector(new DummyMetric(), server, null, Collections.emptyMap()); + server.addConnector(connector); + server.setHandler(new HelloWorldHandler()); + server.start(); + + SimpleHttpClient client = new SimpleHttpClient(null, connector.getLocalPort(), false); + SimpleHttpClient.RequestExecutor ex = client.newGet("/blaasdfnb"); + SimpleHttpClient.ResponseValidator val = ex.execute(); + val.expectContent(equalTo("Hello world")); + } finally { + try { + server.stop(); + } catch (Exception e) { + //ignore + } + } + } + + @Test + public void requireThatPreBoundChannelWorks() throws Exception { + Server server = new Server(); + try { + ServerSocketChannel serverChannel = ServerSocketChannel.open(); + serverChannel.socket().bind(new InetSocketAddress(0)); + + ConnectorFactory factory = new ConnectorFactory(new ConnectorConfig(new ConnectorConfig.Builder()), new ThrowingSslKeyStoreFactory(), new ThrowingSecretStore()); + ConnectorFactory.JDiscServerConnector connector = (ConnectorFactory.JDiscServerConnector) factory.createConnector(new DummyMetric(), server, serverChannel, Collections.emptyMap()); + server.addConnector(connector); + server.setHandler(new HelloWorldHandler()); + server.start(); + + SimpleHttpClient client = new SimpleHttpClient(null, connector.getLocalPort(), false); + SimpleHttpClient.RequestExecutor ex = client.newGet("/blaasdfnb"); + SimpleHttpClient.ResponseValidator val = ex.execute(); + val.expectContent(equalTo("Hello world")); + } finally { + try { + server.stop(); + } catch (Exception e) { + //ignore + } + } + } + + private static class HelloWorldHandler extends AbstractHandler { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + response.getWriter().write("Hello world"); + response.getWriter().flush(); + response.getWriter().close(); + baseRequest.setHandled(true); + } + } + + private static class DummyMetric implements Metric { + @Override + public void set(String key, Number val, Context ctx) { } + + @Override + public void add(String key, Number val, Context ctx) { } + + @Override + public Context createContext(Map<String, ?> properties) { + return new DummyContext(); + } + } + + private static class DummyContext implements Metric.Context { + } + + private static final class ThrowingSslKeyStoreFactory implements SslKeyStoreFactory { + + @Override + public SslKeyStore createKeyStore(ReaderForPath certificateFile, ReaderForPath keyFile) { + throw new UnsupportedOperationException("A SSL key store factory component is not available"); + } + + @Override + public SslKeyStore createTrustStore(ReaderForPath certificateFile) { + throw new UnsupportedOperationException("A SSL key store factory component is not available"); + } + + } + + private static final class ThrowingSecretStore implements SecretStore { + + @Override + public String getSecret(String key) { + throw new UnsupportedOperationException("A secret store is not available"); + } + + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java new file mode 100644 index 00000000000..ae529fdfcca --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.testng.annotations.Test; + +/** + * Check basic error message formatting. Do note these tests are sensitive to + * the line numbering in this file. (And that's a feature, not a bug.) + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class ExceptionWrapperTest { + + @Test + public final void requireNoMessageIsOK() { + final Throwable t = new Throwable(); + final ExceptionWrapper e = new ExceptionWrapper(t); + final String expected = "Throwable() at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:19)"; + + assertThat(e.getMessage(), equalTo(expected)); + } + + @Test + public final void requireAllWrappedLevelsShowUp() { + final Throwable t0 = new Throwable("t0"); + final Throwable t1 = new Throwable("t1", t0); + final Throwable t2 = new Throwable("t2", t1); + final ExceptionWrapper e = new ExceptionWrapper(t2); + final String expected = "Throwable(\"t2\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:30):" + + " Throwable(\"t1\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:29):" + + " Throwable(\"t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:28)"; + + assertThat(e.getMessage(), equalTo(expected)); + } + + @Test + public final void requireMixOfMessageAndNoMessageWorks() { + final Throwable t0 = new Throwable("t0"); + final Throwable t1 = new Throwable(t0); + final Throwable t2 = new Throwable("t2", t1); + final ExceptionWrapper e = new ExceptionWrapper(t2); + final String expected = "Throwable(\"t2\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:43):" + + " Throwable(\"java.lang.Throwable: t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:42):" + + " Throwable(\"t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:41)"; + + assertThat(e.getMessage(), equalTo(expected)); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java new file mode 100644 index 00000000000..e9866a18d7c --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java @@ -0,0 +1,513 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.chain.RequestFilterChain; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.filter.chain.ResponseFilterChain; +import com.yahoo.jdisc.http.filter.ResponseHeaderFilter; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.http.server.FilterBindings; +import org.mockito.ArgumentCaptor; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class FilterTestCase { + @Test + public void requireThatRequestFilterIsNotRunOnUnboundPath() throws Exception { + final RequestFilter filter = mock(RequestFilterMockBase.class); + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + requestFilters.bind("http://*/filtered/*", filter); + final BindingRepository<ResponseFilter> responseFilters = null; + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterIsRunOnBoundPath() throws Exception { + final RequestFilter filter = mock(RequestFilterMockBase.class); + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + requestFilters.bind("http://*/filtered/*", filter); + final BindingRepository<ResponseFilter> responseFilters = null; + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterChangesAreSeenByRequestHandler() throws Exception { + final RequestFilter filter = new HeaderRequestFilter("foo", "bar"); + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + requestFilters.bind("http://*/*", filter); + final BindingRepository<ResponseFilter> responseFilters = null; + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + assertThat(requestHandler.getHeaderMap().get("foo").get(0), is("bar")); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterCanRespond() throws Exception { + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + requestFilters.bind("http://*/*", new RespondForbiddenFilter()); + final BindingRepository<ResponseFilter> responseFilters = null; + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.FORBIDDEN)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatFilterCanHaveNullCompletionHandler() throws Exception { + final int responseStatus = Response.Status.OK; + final String responseMessage = "Excellent"; + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + requestFilters.bind("http://*/*", new NullCompletionHandlerFilter(responseStatus, responseMessage)); + final BindingRepository<ResponseFilter> responseFilters = null; + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html") + .expectStatusCode(is(responseStatus)) + .expectContent(is(responseMessage)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterExecutionIsExceptionSafe() throws Exception { + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + final BindingRepository<ResponseFilter> responseFilters = null; + requestFilters.bind("http://*/*", new ThrowingRequestFilter()); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.INTERNAL_SERVER_ERROR)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterIsNotRunOnUnboundPath() throws Exception { + final ResponseFilter filter = mock(ResponseFilterMockBase.class); + final BindingRepository<RequestFilter> requestFilters = null; + final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>(); + responseFilters.bind("http://*/filtered/*", filter); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, never()).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterIsRunOnBoundPath() throws Exception { + final ResponseFilter filter = mock(ResponseFilterMockBase.class); + final BindingRepository<RequestFilter> requestFilters = null; + final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>(); + responseFilters.bind("http://*/filtered/*", filter); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterChangesAreWrittenToResponse() throws Exception { + final BindingRepository<RequestFilter> requestFilters = null; + final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>(); + responseFilters.bind("http://*/*", new HeaderResponseFilter("foo", "bar")); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html") + .expectHeader("foo", is("bar")); + + assertThat(requestHandler.awaitInvocation(), is(true)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterExecutionIsExceptionSafe() throws Exception { + final BindingRepository<RequestFilter> requestFilters = null; + final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>(); + responseFilters.bind("http://*/*", new ThrowingResponseFilter()); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.INTERNAL_SERVER_ERROR)); + + assertThat(requestHandler.awaitInvocation(), is(true)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterAndResponseFilterCanBindToSamePath() throws Exception { + final String uriPattern = "http://*/*"; + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + final RequestFilter requestFilter = mock(RequestFilterMockBase.class); + requestFilters.bind(uriPattern, requestFilter); + final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>(); + final ResponseFilter responseFilter = mock(ResponseFilterMockBase.class); + responseFilters.bind(uriPattern, responseFilter); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(requestFilter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(responseFilter, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFromRequestFilterGoesThroughResponseFilter() throws Exception { + final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>(); + requestFilters.bind("http://*/*", new RespondForbiddenFilter()); + final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>(); + responseFilters.bind("http://*/*", new HeaderResponseFilter("foo", "bar")); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters); + + testDriver.client().get("/status.html") + .expectStatusCode(is(Response.Status.FORBIDDEN)) + .expectHeader("foo", is("bar")); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterChainRetainsFilters() { + final RequestFilter requestFilter1 = mock(RequestFilter.class); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + + verify(requestFilter1, never()).refer(); + verify(requestFilter2, never()).refer(); + final ResourceReference reference1 = mock(ResourceReference.class); + final ResourceReference reference2 = mock(ResourceReference.class); + when(requestFilter1.refer()).thenReturn(reference1); + when(requestFilter2.refer()).thenReturn(reference2); + final RequestFilter chain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + verify(requestFilter1, times(1)).refer(); + verify(requestFilter2, times(1)).refer(); + + verify(reference1, never()).close(); + verify(reference2, never()).close(); + chain.release(); + verify(reference1, times(1)).close(); + verify(reference2, times(1)).close(); + } + + @Test + public void requireThatRequestFilterChainIsRun() throws Exception { + final RequestFilter requestFilter1 = mock(RequestFilter.class); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + final HttpRequest request = null; + final ResponseHandler responseHandler = null; + requestFilterChain.filter(request, responseHandler); + verify(requestFilter1).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(requestFilter2).filter(any(HttpRequest.class), any(ResponseHandler.class)); + } + + @Test + public void requireThatRequestFilterChainCallsFilterWithOriginalRequest() throws Exception { + final RequestFilter requestFilter = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter); + final HttpRequest request = mock(HttpRequest.class); + final ResponseHandler responseHandler = null; + requestFilterChain.filter(request, responseHandler); + + // Check that the filter is called with the same request argument as the chain was, + // in a manner that allows the request object to be wrapped. + final ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + verify(requestFilter).filter(requestCaptor.capture(), any(ResponseHandler.class)); + verify(request, never()).getUri(); + requestCaptor.getValue().getUri(); + verify(request, times(1)).getUri(); + } + + @Test + public void requireThatRequestFilterChainCallsFilterWithOriginalResponseHandler() throws Exception { + final RequestFilter requestFilter = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter); + final HttpRequest request = null; + final ResponseHandler responseHandler = mock(ResponseHandler.class); + requestFilterChain.filter(request, responseHandler); + + // Check that the filter is called with the same response handler argument as the chain was, + // in a manner that allows the handler object to be wrapped. + final ArgumentCaptor<ResponseHandler> responseHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + verify(requestFilter).filter(any(HttpRequest.class), responseHandlerCaptor.capture()); + verify(responseHandler, never()).handleResponse(any(Response.class)); + responseHandlerCaptor.getValue().handleResponse(mock(Response.class)); + verify(responseHandler, times(1)).handleResponse(any(Response.class)); + } + + @Test + public void requireThatRequestFilterCanTerminateChain() throws Exception { + final RequestFilter requestFilter1 = new RespondForbiddenFilter(); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + final HttpRequest request = null; + final ResponseHandler responseHandler = mock(ResponseHandler.class); + when(responseHandler.handleResponse(any(Response.class))).thenReturn(mock(ContentChannel.class)); + + requestFilterChain.filter(request, responseHandler); + + verify(requestFilter2, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + final ArgumentCaptor<Response> responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(responseHandler).handleResponse(responseCaptor.capture()); + assertThat(responseCaptor.getValue().getStatus(), is(Response.Status.FORBIDDEN)); + } + + @Test + public void requireThatResponseFilterChainRetainsFilters() { + final ResponseFilter responseFilter1 = mock(ResponseFilter.class); + final ResponseFilter responseFilter2 = mock(ResponseFilter.class); + + verify(responseFilter1, never()).refer(); + verify(responseFilter2, never()).refer(); + final ResourceReference reference1 = mock(ResourceReference.class); + final ResourceReference reference2 = mock(ResourceReference.class); + when(responseFilter1.refer()).thenReturn(reference1); + when(responseFilter2.refer()).thenReturn(reference2); + final ResponseFilter chain = ResponseFilterChain.newInstance(responseFilter1, responseFilter2); + verify(responseFilter1, times(1)).refer(); + verify(responseFilter2, times(1)).refer(); + + verify(reference1, never()).close(); + verify(reference2, never()).close(); + chain.release(); + verify(reference1, times(1)).close(); + verify(reference2, times(1)).close(); + } + + @Test + public void requireThatResponseFilterChainIsRun() { + final ResponseFilter responseFilter1 = new ResponseHeaderFilter("foo", "bar"); + final ResponseFilter responseFilter2 = mock(ResponseFilter.class); + final int statusCode = Response.Status.BAD_GATEWAY; + final Response response = new Response(statusCode); + final Request request = null; + + ResponseFilterChain.newInstance(responseFilter1, responseFilter2).filter(response, request); + + final ArgumentCaptor<Response> responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(responseFilter2).filter(responseCaptor.capture(), any(Request.class)); + assertThat(responseCaptor.getValue().getStatus(), is(statusCode)); + assertThat(responseCaptor.getValue().headers().getFirst("foo"), is("bar")); + + assertThat(response.getStatus(), is(statusCode)); + assertThat(response.headers().getFirst("foo"), is("bar")); + } + + private static TestDriver newDriver( + final MyRequestHandler requestHandler, + final BindingRepository<RequestFilter> requestFilters, + final BindingRepository<ResponseFilter> responseFilters) + throws IOException { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newFilterModule(requestFilters, responseFilters)); + } + + private static com.google.inject.Module newFilterModule( + final BindingRepository<RequestFilter> requestFilters, + final BindingRepository<ResponseFilter> responseFilters) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class).toInstance( + new FilterBindings( + requestFilters != null ? requestFilters : EMPTY_REQUEST_FILTER_REPOSITORY, + responseFilters != null ? responseFilters : EMPTY_RESPONSE_FILTER_REPOSITORY)); + bind(ServerConfig.class).toInstance(new ServerConfig(new ServerConfig.Builder())); + } + }, + new ConnectorFactoryRegistryModule()); + } + + private static final BindingRepository<RequestFilter> EMPTY_REQUEST_FILTER_REPOSITORY = new BindingRepository<>(); + private static final BindingRepository<ResponseFilter> EMPTY_RESPONSE_FILTER_REPOSITORY = new BindingRepository<>(); + + private static abstract class RequestFilterMockBase extends AbstractResource implements RequestFilter {} + private static abstract class ResponseFilterMockBase extends AbstractResource implements ResponseFilter {} + + private static class MyRequestHandler extends AbstractRequestHandler { + private final CountDownLatch invocationLatch = new CountDownLatch(1); + private final AtomicReference<Map<String, List<String>>> headerCopy = new AtomicReference<>(null); + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + try { + headerCopy.set(new HashMap<String, List<String>>(request.headers())); + ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler); + return null; + } finally { + invocationLatch.countDown(); + } + } + + public boolean hasBeenInvokedYet() { + return invocationLatch.getCount() == 0L; + } + + public boolean awaitInvocation() throws InterruptedException { + return invocationLatch.await(60, TimeUnit.SECONDS); + } + + public Map<String, List<String>> getHeaderMap() { + return headerCopy.get(); + } + } + + private static class RespondForbiddenFilter extends AbstractResource implements RequestFilter { + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + ResponseDispatch.newInstance(Response.Status.FORBIDDEN).dispatch(handler); + } + } + + private static class ThrowingRequestFilter extends AbstractResource implements RequestFilter { + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + throw new RuntimeException(); + } + } + + private static class ThrowingResponseFilter extends AbstractResource implements ResponseFilter { + @Override + public void filter(final Response response, final Request request) { + throw new RuntimeException(); + } + } + + private static class HeaderRequestFilter extends AbstractResource implements RequestFilter { + private final String key; + private final String val; + + public HeaderRequestFilter(final String key, final String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + request.headers().add(key, val); + } + } + + private static class HeaderResponseFilter extends AbstractResource implements ResponseFilter { + private final String key; + private final String val; + + public HeaderResponseFilter(final String key, final String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(final Response response, final Request request) { + response.headers().add(key, val); + } + } + + public class NullCompletionHandlerFilter extends AbstractResource implements RequestFilter { + private final int responseStatus; + private final String responseMessage; + + public NullCompletionHandlerFilter(final int responseStatus, final String responseMessage) { + this.responseStatus = responseStatus; + this.responseMessage = responseMessage; + } + + @Override + public void filter(final HttpRequest request, final ResponseHandler responseHandler) { + final HttpResponse response = HttpResponse.newInstance(responseStatus); + final ContentChannel channel = responseHandler.handleResponse(response); + final CompletionHandler completionHandler = null; + channel.write(ByteBuffer.wrap(responseMessage.getBytes()), completionHandler); + channel.close(null); + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java new file mode 100644 index 00000000000..1ed15d6d380 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java @@ -0,0 +1,549 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.Principal; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; + +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.Response; +import org.testng.annotations.Test; + +import com.google.inject.Key; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; + +import static org.testng.AssertJUnit.fail; + +/** + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class HttpRequestFactoryTest { + + private static final class MockRequest implements HttpServletRequest { + String queryString = null; + StringBuffer requestUrl; + final String method = "GET"; + final String protocol = "HTTP/1.1"; + final String remoteAddr = "127.0.0.1"; + final int remotePort = 0; + + public MockRequest(String sortOfUri) { + int mark = sortOfUri.indexOf('?'); + if (mark > 0) { + queryString = sortOfUri.substring(mark + 1); + requestUrl = new StringBuffer(sortOfUri.substring(0, mark)); + } else { + queryString = null; + requestUrl = new StringBuffer(sortOfUri); + } + } + + @Override + public Object getAttribute(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration<String> getAttributeNames() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getCharacterEncoding() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setCharacterEncoding(String env) + throws UnsupportedEncodingException { + // TODO Auto-generated method stub + + } + + @Override + public int getContentLength() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long getContentLengthLong() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getContentType() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getParameter(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration<String> getParameterNames() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String[] getParameterValues(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map<String, String[]> getParameterMap() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getScheme() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getServerName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getServerPort() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public BufferedReader getReader() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getRemoteAddr() { + return remoteAddr; + } + + @Override + public String getRemoteHost() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setAttribute(String name, Object o) { + // TODO Auto-generated method stub + + } + + @Override + public void removeAttribute(String name) { + // TODO Auto-generated method stub + + } + + @Override + public Locale getLocale() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration<Locale> getLocales() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isSecure() { + // TODO Auto-generated method stub + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + @Deprecated + public String getRealPath(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getRemotePort() { + return remotePort; + } + + @Override + public String getLocalName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getLocalAddr() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getLocalPort() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public ServletContext getServletContext() { + // TODO Auto-generated method stub + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + // TODO Auto-generated method stub + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, + ServletResponse servletResponse) throws IllegalStateException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isAsyncStarted() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isAsyncSupported() { + // TODO Auto-generated method stub + return false; + } + + @Override + public AsyncContext getAsyncContext() { + // TODO Auto-generated method stub + return null; + } + + @Override + public DispatcherType getDispatcherType() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getAuthType() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Cookie[] getCookies() { + // TODO Auto-generated method stub + return null; + } + + @Override + public long getDateHeader(String name) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getHeader(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration<String> getHeaders(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration<String> getHeaderNames() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getIntHeader(String name) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String getPathInfo() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPathTranslated() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getContextPath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public String getRemoteUser() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isUserInRole(String role) { + // TODO Auto-generated method stub + return false; + } + + @Override + public Principal getUserPrincipal() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getRequestedSessionId() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getRequestURI() { + // TODO Auto-generated method stub + return null; + } + + @Override + public StringBuffer getRequestURL() { + return requestUrl; + } + + @Override + public String getServletPath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public HttpSession getSession(boolean create) { + // TODO Auto-generated method stub + return null; + } + + @Override + public HttpSession getSession() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String changeSessionId() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isRequestedSessionIdValid() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + // TODO Auto-generated method stub + return false; + } + + @Override + @Deprecated + public boolean isRequestedSessionIdFromUrl() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean authenticate(HttpServletResponse response) + throws IOException, ServletException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void login(String username, String password) + throws ServletException { + // TODO Auto-generated method stub + + } + + @Override + public void logout() throws ServletException { + // TODO Auto-generated method stub + + } + + @Override + public Collection<Part> getParts() throws IOException, ServletException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + // TODO Auto-generated method stub + return null; + } + + @Override + public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) + throws IOException, ServletException { + // TODO Auto-generated method stub + return null; + } + + } + + @Test + public final void test() { + String noise = "query=a" + "\\" + "^{|}&other=madeit"; + HttpServletRequest servletRequest = new MockRequest( + "http://yahoo.com/search?" + noise); + HttpRequest request = HttpRequestFactory.newJDiscRequest( + new MockContainer(), servletRequest); + assertThat(request.getUri().getQuery(), equalTo(noise)); + } + + @Test + public final void testIllegalQuery() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + new MockRequest("http://example.com/search?query=\"contains_quotes\"")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + private static final class MockContainer implements CurrentContainer { + + @Override + public Container newReference(URI uri) { + return new Container() { + + @Override + public RequestHandler resolveHandler(com.yahoo.jdisc.Request request) { + return null; + } + + @Override + public <T> T getInstance(Key<T> tKey) { + return null; + } + + @Override + public <T> T getInstance(Class<T> tClass) { + return null; + } + + @Override + public ResourceReference refer() { + return References.NOOP_REFERENCE; + } + + @Override + public void release() { + + } + + @Override + public long currentTimeMillis() { + return 0; + } + }; + } + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java new file mode 100644 index 00000000000..b973a7e34bc --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java @@ -0,0 +1,818 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.http.server.FilterBindings; +import com.yahoo.jdisc.test.ServerProviderConformanceTest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.regex.Pattern; + +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.cthul.matchers.CthulMatchers.containsPattern; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.any; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class HttpServerConformanceTest extends ServerProviderConformanceTest { + + private static final String REQUEST_CONTENT = "myRequestContent"; + private static final String RESPONSE_CONTENT = "myResponseContent"; + + @AfterClass + public static void reportDiagnostics() { + System.out.println( + "After " + HttpServerConformanceTest.class.getSimpleName() + + ": #threads=" + Thread.getAllStackTraces().size()); + } + + @Override + @Test + public void testContainerNotReadyException() throws Throwable { + new TestRunner().expect(errorWithReason(is(SC_INTERNAL_SERVER_ERROR), containsString("Container not ready."))) + .execute(); + } + + @Override + @Test + public void testBindingSetNotFoundException() throws Throwable { + new TestRunner().expect(errorWithReason(is(SC_NOT_FOUND), containsString("No binding set named 'unknown'."))) + .execute(); + } + + @Override + @Test + public void testNoBindingSetSelectedException() throws Throwable { + final Pattern reasonPattern = Pattern.compile(".*No binding set selected for URI 'http://.+/status.html'\\."); + new TestRunner().expect(errorWithReason(is(SC_INTERNAL_SERVER_ERROR), matchesPattern(reasonPattern))) + .execute(); + } + + @Override + @Test + public void testBindingNotFoundException() throws Throwable { + final Pattern contentPattern = Pattern.compile("No binding for URI 'http://.+/status.html'\\."); + new TestRunner().expect(errorWithReason(is(NOT_FOUND), containsPattern(contentPattern))) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncCloseResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncWriteResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestException() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncCloseResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncWriteResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(anyOf(successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicException() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicException() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testResponseWriteCompletionException() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testResponseCloseCompletionException() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testResponseCloseCompletionExceptionNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + private static Matcher<ResponseGist> success() { + final Matcher<Integer> expectedStatusCode = is(OK); + final Matcher<String> expectedReasonPhrase = is("OK"); + final Matcher<String> expectedContent = is(RESPONSE_CONTENT); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher<ResponseGist> successNoContent() { + final Matcher<Integer> expectedStatusCode = is(OK); + final Matcher<String> expectedReasonPhrase = is("OK"); + final Matcher<String> expectedContent = is(""); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher<ResponseGist> serverError() { + final Matcher<Integer> expectedStatusCode = is(INTERNAL_SERVER_ERROR); + final Matcher<String> expectedReasonPhrase = any(String.class); + final Matcher<String> expectedContent = containsString(ConformanceException.class.getSimpleName()); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher<ResponseGist> errorWithReason( + final Matcher<Integer> expectedStatusCode, final Matcher<String> expectedReasonPhrase) { + final Matcher<String> expectedContent = any(String.class); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher<ResponseGist> responseMatcher( + final Matcher<Integer> expectedStatusCode, + final Matcher<String> expectedReasonPhrase, + final Matcher<String> expectedContent) { + return new TypeSafeMatcher<ResponseGist>() { + @Override + public void describeTo(final Description description) { + description.appendText("status code "); + expectedStatusCode.describeTo(description); + description.appendText(", reason "); + expectedReasonPhrase.describeTo(description); + description.appendText(" and content "); + expectedContent.describeTo(description); + } + + @Override + protected void describeMismatchSafely( + final ResponseGist response, final Description mismatchDescription) { + mismatchDescription.appendText(" status code was ").appendValue(response.getStatusCode()) + .appendText(", reason was ").appendValue(response.getReasonPhrase()) + .appendText(" and content was ").appendValue(response.getContent()); + } + + @Override + protected boolean matchesSafely(final ResponseGist response) { + return expectedStatusCode.matches(response.getStatusCode()) + && expectedReasonPhrase.matches(response.getReasonPhrase()) + && expectedContent.matches(response.getContent()); + } + }; + } + + private static class ResponseGist { + private final int statusCode; + private final String content; + private String reasonPhrase; + + public ResponseGist(int statusCode, String reasonPhrase, String content) { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.content = content; + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() { + return content; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + @Override + public String toString() { + return "ResponseGist {" + + " statusCode=" + statusCode + + " reasonPhrase=" + reasonPhrase + + " content=" + content + + " }"; + } + } + + @SuppressWarnings("deprecation") + private class TestRunner implements Adapter<JettyHttpServer, ClientProxy, Future<HttpResponse>> { + + private Matcher<ResponseGist> expectedResponse = null; + HttpVersion requestVersion; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + void execute() throws Throwable { + requestVersion = HttpVersion.HTTP_1_0; + runTest(this); + + requestVersion = HttpVersion.HTTP_1_1; + runTest(this); + + executorService.shutdown(); + } + + TestRunner expect(final Matcher<ResponseGist> matcher) { + expectedResponse = matcher; + return this; + } + + @Override + public Module newConfigModule() { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class) + .toInstance(new FilterBindings( + new BindingRepository<>(), + new BindingRepository<>())); + bind(ServerConfig.class) + .toInstance(new ServerConfig(new ServerConfig.Builder())); + } + }, + new ConnectorFactoryRegistryModule()); + } + + @Override + public Class<JettyHttpServer> getServerProviderClass() { + return JettyHttpServer.class; + } + + @Override + public ClientProxy newClient(final JettyHttpServer server) throws Throwable { + return new ClientProxy(server.getListenPort(), requestVersion); + } + + @Override + public Future<HttpResponse> executeRequest( + final ClientProxy client, + final boolean withRequestContent) throws Throwable { + final HttpUriRequest request; + final URI requestUri = URI.create("http://localhost:" + client.listenPort + "/status.html"); + if (!withRequestContent) { + HttpGet httpGet = new HttpGet(requestUri); + httpGet.setProtocolVersion(client.requestVersion); + request = httpGet; + } else { + final HttpPost post = new HttpPost(requestUri); + post.setEntity(new StringEntity(REQUEST_CONTENT, StandardCharsets.UTF_8)); + post.setProtocolVersion(client.requestVersion); + request = post; + } + System.out.println("executorService:" + + " .isShutDown()=" + executorService.isShutdown() + + " .isTerminated()=" + executorService.isTerminated()); + return executorService.submit(() -> client.delegate.execute(request)); + } + + @Override + public Iterable<ByteBuffer> newResponseContent() { + return Collections.singleton(StandardCharsets.UTF_8.encode(RESPONSE_CONTENT)); + } + + @Override + public void validateResponse(final Future<HttpResponse> responseFuture) throws Throwable { + final HttpResponse response = responseFuture.get(); + final ResponseGist responseGist = new ResponseGist( + response.getStatusLine().getStatusCode(), + response.getStatusLine().getReasonPhrase(), + EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); + assertThat(responseGist, expectedResponse); + } + } + + private static class ClientProxy { + + final HttpClient delegate; + final int listenPort; + final ProtocolVersion requestVersion; + + ClientProxy(final int listenPort, final HttpVersion requestVersion) { + this.delegate = HttpClientBuilder.create().build(); + this.requestVersion = requestVersion; + this.listenPort = listenPort; + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java new file mode 100644 index 00000000000..cf3721eef88 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.MetricConsumer; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; + +import static com.yahoo.jdisc.Response.Status.OK; +import static org.cthul.matchers.CthulMatchers.isA; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class HttpServerMetricTest { + + @Test(enabled = false) + public void requireThatNumActiveRequestsIsTracked() throws Exception { + final MetricConsumer metricConsumer = mock(MetricConsumer.class); + final TestDriver driver = TestDrivers.newInstance( + new EchoRequestHandler(), + newMetricModule(metricConsumer)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + final InOrder order = inOrder(metricConsumer); + order.verify(metricConsumer).set(eq("serverNumActiveRequests"), eq(1), any(Metric.Context.class)); + order.verify(metricConsumer).set(eq("serverNumActiveRequests"), eq(0), any(Metric.Context.class)); + assertThat(driver.close(), is(true)); + } + + @SuppressWarnings("deprecation") + @Test(enabled = false) + public void requireThatCustomMetricDimensionsAreSupported() throws Exception { + final MetricConsumer metricConsumer = mock(MetricConsumer.class); + // TODO: enable metrics + final ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder(); + + final Map<String, String> commonDimensions = new HashMap<>(); + commonDimensions.put("key1", "value1"); + commonDimensions.put("key2", "value2"); + // TODO: serverConfig.commonMetricDimensions().add(...); + + final TestDriver driver = TestDrivers.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder(), + connectorConfig, + newMetricModule(metricConsumer)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + + final ArgumentCaptor<Map<String, ?>> contextCaptor = new ArgumentCaptor<>(); + verify(metricConsumer).createContext(contextCaptor.capture()); + final Map<String, ?> actualContext = contextCaptor.getValue(); + for (final Map.Entry<String, String> entry : commonDimensions.entrySet()) { + assertThat(actualContext.get(entry.getKey()), isA(String.class).that(is(entry.getValue()))); + } + assertThat(actualContext.get("serverName"), isA(String.class).that(matchesPattern("\\S+"))); + assertThat(actualContext.get("serverPort"), isA(String.class).that(matchesPattern("\\d+"))); + assertThat(driver.close(), is(true)); + } + + private static Module newMetricModule(final MetricConsumer metricConsumer) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(MetricConsumer.class).toInstance(metricConsumer); + } + }; + } + + private static class EchoRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return handler.handleResponse(new Response(OK)); + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java new file mode 100644 index 00000000000..5ef0f7db742 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java @@ -0,0 +1,710 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.jdisc.References; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.content.StringBody; +import org.testng.annotations.Test; + +import java.net.BindException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static com.yahoo.jdisc.Response.Status.REQUEST_TIMEOUT; +import static com.yahoo.jdisc.Response.Status.REQUEST_URI_TOO_LONG; +import static com.yahoo.jdisc.Response.Status.UNSUPPORTED_MEDIA_TYPE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.CONNECTION; +import static com.yahoo.jdisc.http.HttpHeaders.Names.CONTENT_TYPE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.COOKIE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING; +import static com.yahoo.jdisc.http.HttpHeaders.Names.X_TRACE_ID; +import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED; +import static com.yahoo.jdisc.http.HttpHeaders.Values.CLOSE; +import static com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; +import static org.cthul.matchers.CthulMatchers.containsPattern; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class HttpServerTest { + + @Test + public void requireThatServerCanListenToRandomPort() throws Exception { + final TestDriver driver = TestDrivers.newInstance(mockRequestHandler()); + assertThat(driver.server().getListenPort(), is(not(0))); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanNotListenToBoundPort() throws Exception { + final TestDriver driver = TestDrivers.newInstance(mockRequestHandler()); + try { + TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .listenPort(driver.server().getListenPort()) + ); + } catch (final Throwable t) { + assertThat(t.getCause(), instanceOf(BindException.class)); + } + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatBindingSetNotFoundReturns404() throws Exception { + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder() + .developerMode(true), + new ConnectorConfig.Builder(), + newBindingSetSelector("unknown")); + driver.client().get("/status.html") + .expectStatusCode(is(NOT_FOUND)) + .expectContent(containsPattern(Pattern.compile( + Pattern.quote(BindingSetNotFoundException.class.getName()) + + ": No binding set named 'unknown'\\.\n\tat .+", + Pattern.DOTALL | Pattern.MULTILINE))); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatTooLongInitLineReturns414() throws Exception { + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .requestHeaderSize(1)); + driver.client().get("/status.html") + .expectStatusCode(is(REQUEST_URI_TOO_LONG)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanEcho() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanEchoCompressed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + SimpleHttpClient client = driver.newClient(true); + client.get("/status.html") + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanHandleMultipleRequests() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final String requestContent = generateContent('a', 30); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostDoesNotRemoveContentByDefault() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}foo=bar")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostKeepsContentWhenConfiguredTo() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), false); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}foo=bar")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostRemovesContentWhenConfiguredTo() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), true); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostWithCharsetSpecifiedWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final String requestContent = generateContent('a', 30); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(X_DISABLE_CHUNKING, "true") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=UTF-8") + .setContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatEmptyFormPostWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormParametersAreParsed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("a=b&c=d") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{a=[b], c=[d]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatUriParametersAreParsed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html?a=b&c=d") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{a=[b], c=[d]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormAndUriParametersAreMerged() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html?a=b&c=d1") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("c=d2&e=f") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{a=[b], c=[d1, d2], e=[f]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormCharsetIsHonored() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), true); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=ISO-8859-1") + .setBinaryContent(new byte[]{66, (byte) 230, 114, 61, 98, 108, (byte) 229}) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{B\u00e6r=[bl\u00e5]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatUnknownFormCharsetIsTreatedAsBadRequest() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=FLARBA-GARBA-7") + .setContent("a=b") + .execute(); + response.expectStatusCode(is(UNSUPPORTED_MEDIA_TYPE)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostWithPercentEncodedContentIsDecoded() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("%20%3D%C3%98=%22%25+") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{ =\u00d8=[\"% ]}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatFormPostWithThrowingHandlerIsExceptionSafe() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ThrowingHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("a=b") + .execute(); + response.expectStatusCode(is(INTERNAL_SERVER_ERROR)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatMultiPostWorks() throws Exception { + // This is taken from tcpdump of bug 5433352 and reassembled here to see that httpserver passes things on. + final String startTxtContent = "this is a test for POST."; + final String updaterConfContent + = "identifier = updater\n" + + "server_type = gds\n"; + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .setMultipartContent( + newFileBody("", "start.txt", startTxtContent), + newFileBody("", "updater.conf", updaterConfContent)) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(containsString(startTxtContent)) + .expectContent(containsString(updaterConfContent)); + } + + @Test + public void requireThatRequestCookiesAreReceived() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new CookiePrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(COOKIE, "foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(containsString("[foo=bar]")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatSetCookieHeaderIsCorrect() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new CookieSetterRequestHandler( + new Cookie("foo", "bar").setComment("comment yeah") + .setCommentURL("http://comment.yes/") + .setDiscard(true) + .setDomain(".localhost") + .setHttpOnly(true) + .setMaxAge(5000, TimeUnit.SECONDS) + .setPath("/foopath") + .setSecure(true) + .setVersion(2))); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader("Set-Cookie", is("foo=bar; " + + "Max-Age=5000; " + + "Path=\"/foopath\"; " + + "Domain=.localhost; " + + "Secure; HTTPOnly; " + + "Comment=\"comment yeah\"; " + + "Version=1; " + + "CommentURL=\"http://comment.yes/\"; " + + "Discard")); + assertThat(driver.close(), is(true)); + } + + @Test(enabled = false) + public void requireThatGeneratedTraceIdIsSet() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + final SimpleHttpClient client1 = driver.client(); + final SimpleHttpClient client2 = driver.newClient(); + + client1.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute() + .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000000")); + client1.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute() + .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000001")); + client2.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute() + .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000000")); + client1.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute() + .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000002")); + client2.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute() + .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000001")); + client2.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute() + .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000002")); + + assertThat(driver.close(), is(true)); + } + + @Test(enabled = false) + public void requireThatClientTraceIdIsSet() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().newGet("/status.html").addHeader(X_TRACE_ID, "foo").execute() + .expectHeader(X_TRACE_ID, is("foo")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatTimeoutWorks() throws Exception { + final UnresponsiveHandler requestHandler = new UnresponsiveHandler(); + final TestDriver driver = TestDrivers.newInstance(requestHandler); + driver.client().get("/status.html") + .expectStatusCode(is(REQUEST_TIMEOUT)); + ResponseDispatch.newInstance(OK).dispatch(requestHandler.responseHandler); + assertThat(driver.close(), is(true)); + } + + // Header with no value is disallowed by https://tools.ietf.org/html/rfc7230#section-3.2 + @Test + public void requireThatHeaderWithNullValueIsOmitted() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", null)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectNoHeader("X-Foo"); + assertThat(driver.close(), is(true)); + } + + // Header with no value is disallowed by https://tools.ietf.org/html/rfc7230#section-3.2 + @Test + public void requireThatHeaderWithEmptyValueIsOmitted() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", "")); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectNoHeader("X-Foo"); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatNoConnectionHeaderMeansKeepAliveInHttp11KeepAliveDisabled() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler(CONNECTION, CLOSE)); + driver.client().get("/status.html") + .expectHeader(CONNECTION, is(CLOSE)); + assertThat(driver.close(), is(true)); + } + + @Test(enabled = false) + public void requireThatRequestTrailersAreSupported() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new RequestHandlerThatEchoesTrailers()); + assertThat(driver.client().raw("GET /status.html HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Transfer-Encoding: chunked\r\n\r\n" + + "0\r\n" + + "X-Foo: foo\r\n" + + "X-Bar: bar\r\n" + + "\r\n"), + containsPattern(Pattern.quote("{X-Bar=[bar], X-Foo=[foo]}"))); + assertThat(driver.close(), is(true)); + } + + @Test(enabled = false) + public void requireThatResponseTrailersAreSupported() throws Exception { + final HeaderFields trailers = new HeaderFields(); + trailers.add("X-Foo", "foo"); + trailers.add("X-Bar", "bar"); + final TestDriver driver = TestDrivers.newInstance(new RequestHandlerThatSetsResponseTrailers(trailers)); + driver.client().get("/status.html") + .expectTrailer("X-Foo", is("foo")) + .expectTrailer("X-Bar", is("bar")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanRespondToSslRequest() throws Exception { + final TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatConnectedAtReturnsNonZero() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ConnectedAtRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectContent(matchesPattern("\\d{13,}")); + assertThat(driver.close(), is(true)); + } + + private static RequestHandler mockRequestHandler() { + final RequestHandler mockRequestHandler = mock(RequestHandler.class); + when(mockRequestHandler.refer()).thenReturn(References.NOOP_REFERENCE); + return mockRequestHandler; + } + + private static String generateContent(final char c, final int len) { + final StringBuilder ret = new StringBuilder(len); + for (int i = 0; i < len; ++i) { + ret.append(c); + } + return ret.toString(); + } + + private static TestDriver newDriverWithFormPostContentRemoved( + final RequestHandler requestHandler, final boolean removeFormPostBody) throws Exception { + return TestDrivers.newConfiguredInstance( + requestHandler, + new ServerConfig.Builder() + .removeRawPostBodyForWwwUrlEncodedPost(removeFormPostBody), + new ConnectorConfig.Builder()); + } + + private static FormBodyPart newFileBody(final String parameterName, final String fileName, final String fileContent) + throws Exception { + return new FormBodyPart( + parameterName, + new StringBody(fileContent) { + @Override + public String getFilename() { + return fileName; + } + + @Override + public String getTransferEncoding() { + return "binary"; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public String getCharset() { + return null; + } + }); + } + + private static class ConnectedAtRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpRequest httpRequest = (HttpRequest)request; + final String connectedAt = String.valueOf(httpRequest.getConnectedAt(TimeUnit.MILLISECONDS)); + final ContentChannel ch = handler.handleResponse(new Response(OK)); + ch.write(ByteBuffer.wrap(connectedAt.getBytes(StandardCharsets.UTF_8)), null); + ch.close(null); + return null; + } + } + + private static class CookieSetterRequestHandler extends AbstractRequestHandler { + + final Cookie cookie; + + CookieSetterRequestHandler(final Cookie cookie) { + this.cookie = cookie; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpResponse response = HttpResponse.newInstance(OK); + response.encodeSetCookieHeader(Collections.singletonList(cookie)); + ResponseDispatch.newInstance(response).dispatch(handler); + return null; + } + } + + private static class CookiePrinterRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final List<Cookie> cookies = new ArrayList<>(((HttpRequest)request).decodeCookieHeader()); + Collections.sort(cookies, new CookieComparator()); + final ContentChannel out = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + out.write(StandardCharsets.UTF_8.encode(cookies.toString()), null); + out.close(null); + return null; + } + } + + private static class ParameterPrinterRequestHandler extends AbstractRequestHandler { + private static final CompletionHandler NULL_COMPLETION_HANDLER = null; + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final Map<String, List<String>> parameters = + new TreeMap<>(((HttpRequest)request).parameters()); + final ContentChannel responseContentChannel + = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + responseContentChannel.write( + ByteBuffer.wrap(parameters.toString().getBytes(StandardCharsets.UTF_8)), + NULL_COMPLETION_HANDLER); + + // Have the request content written back to the response. + return responseContentChannel; + } + } + + private static class RequestHandlerThatEchoesTrailers extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpRequest httpRequest = (HttpRequest)request; + final ContentChannel out = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + return new ContentChannel() { + + @Override + public void write(final ByteBuffer buf, final CompletionHandler handler) { + handler.completed(); + } + + @Override + public void close(final CompletionHandler handler) { + synchronized (httpRequest.trailers()) { + out.write(StandardCharsets.UTF_8.encode(httpRequest.trailers().toString()), null); + } + out.close(null); + handler.completed(); + } + }; + } + } + + private static class RequestHandlerThatSetsResponseTrailers extends AbstractRequestHandler { + + final HeaderFields trailers; + + RequestHandlerThatSetsResponseTrailers(final HeaderFields trailers) { + this.trailers = trailers; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpResponse response = HttpResponse.newInstance(OK); + final ContentChannel content = handler.handleResponse(response); + synchronized (response.trailers()) { + response.trailers().putAll(this.trailers); + } + content.close(null); + return null; + } + } + + private static class ThrowingHandler extends AbstractRequestHandler { + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + throw new RuntimeException("Deliberately thrown exception"); + } + } + + private static class UnresponsiveHandler extends AbstractRequestHandler { + + ResponseHandler responseHandler; + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + request.setTimeout(100, TimeUnit.MILLISECONDS); + responseHandler = handler; + return null; + } + } + + private static class EchoRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return handler.handleResponse(new Response(OK)); + } + } + + private static class EchoWithHeaderRequestHandler extends AbstractRequestHandler { + + final String headerName; + final String headerValue; + + EchoWithHeaderRequestHandler(final String headerName, final String headerValue) { + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final Response response = new Response(OK); + response.headers().add(headerName, headerValue); + return handler.handleResponse(response); + } + } + + private static Module newBindingSetSelector(final String setName) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(BindingSetSelector.class).toInstance(new BindingSetSelector() { + + @Override + public String select(final URI uri) { + return setName; + } + }); + } + }; + } + + private static class CookieComparator implements Comparator<Cookie> { + + @Override + public int compare(final Cookie lhs, final Cookie rhs) { + return lhs.getName().compareTo(rhs.getName()); + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java new file mode 100644 index 00000000000..6bbd71c2cee --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.testng.annotations.Test; + +import java.net.URI; + +import static com.yahoo.jdisc.Response.Status.OK; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class JDiscHttpServletTest { + + @Test + public void requireThatServerRespondsToAllMethods() throws Exception { + final TestDriver driver = TestDrivers.newInstance(newEchoHandler()); + final URI uri = driver.client().newUri("/status.html"); + driver.client().execute(new HttpGet(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPost(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpHead(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPut(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpDelete(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpOptions(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpTrace(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPatch(uri)) + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + private static RequestHandler newEchoHandler() { + return new AbstractRequestHandler() { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return handler.handleResponse(new Response(OK)); + } + }; + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java new file mode 100644 index 00000000000..2f250799f1c --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java @@ -0,0 +1,219 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.HttpHeaders; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; + +import javax.net.ssl.SSLContext; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.regex.Pattern; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * A simple http client for testing + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class SimpleHttpClient { + + private final HttpClient delegate; + private final String scheme; + private final int listenPort; + + public SimpleHttpClient(final SSLContext sslContext, final int listenPort, final boolean useCompression) { + HttpClientBuilder builder = HttpClientBuilder.create(); + if (!useCompression) { + builder.disableContentCompression(); + } + if (sslContext != null) { + SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( + sslContext, + SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + builder.setSSLSocketFactory(sslConnectionFactory); + + Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() + .register("https", sslConnectionFactory) + .build(); + builder.setConnectionManager(new BasicHttpClientConnectionManager(registry)); + scheme = "https"; + } else { + scheme = "http"; + } + this.delegate = builder.build(); + this.listenPort = listenPort; + } + + public URI newUri(final String path) { + return URI.create(scheme + "://localhost:" + listenPort + path); + } + + public RequestExecutor newGet(final String path) { + return newRequest(new HttpGet(newUri(path))); + } + + public RequestExecutor newPost(final String path) { + return newRequest(new HttpPost(newUri(path))); + } + + public RequestExecutor newRequest(final HttpUriRequest request) { + return new RequestExecutor().setRequest(request); + } + + public ResponseValidator execute(final HttpUriRequest request) throws IOException { + return newRequest(request).execute(); + } + + public ResponseValidator get(final String path) throws IOException { + return newGet(path).execute(); + } + + public String raw(final String request) throws IOException { + final Socket socket = new Socket("localhost", listenPort); + final OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8); + out.write(request); + out.flush(); + + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + final InputStream in = socket.getInputStream(); + final int[] TERMINATOR = { '\r', '\n', '\r', '\n' }; + for (int pos = 0; pos < TERMINATOR.length; ++pos) { + final int b = in.read(); + if (b < 0) { + throw new EOFException(); + } + if (b != TERMINATOR[pos]) { + pos = -1; + } + buf.write(b); + } + final String response = buf.toString(StandardCharsets.UTF_8.name()); + final java.util.regex.Matcher matcher = Pattern.compile(HttpHeaders.Names.CONTENT_LENGTH + ": (.+)\r\n").matcher(response); + if (matcher.find()) { + final int len = Integer.valueOf(matcher.group(1)); + for (int i = 0; i < len; ++i) { + final int b = in.read(); + if (b < 0) { + throw new EOFException(); + } + buf.write(b); + } + } + + socket.close(); + return buf.toString(StandardCharsets.UTF_8.name()); + } + + public class RequestExecutor { + + private HttpUriRequest request; + private HttpEntity entity; + + public RequestExecutor setRequest(final HttpUriRequest request) { + this.request = request; + return this; + } + + public RequestExecutor addHeader(final String name, final String value) { + this.request.addHeader(name, value); + return this; + } + + public RequestExecutor setContent(final String content) { + this.entity = new StringEntity(content, StandardCharsets.UTF_8); + return this; + } + + public RequestExecutor setBinaryContent(final byte[] content) { + this.entity = new ByteArrayEntity(content); + return this; + } + + public RequestExecutor setMultipartContent(final FormBodyPart... parts) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + Arrays.stream(parts).forEach(part -> builder.addPart(part.getName(), part.getBody())); + this.entity = builder.build(); + return this; + } + + public ResponseValidator execute() throws IOException { + if (entity != null) { + ((HttpPost)request).setEntity(entity); + } + return new ResponseValidator(delegate.execute(request)); + } + } + + public static class ResponseValidator { + + private final HttpResponse response; + private final String content; + + public ResponseValidator(final HttpResponse response) throws IOException { + this.response = response; + + final HttpEntity entity = response.getEntity(); + this.content = entity == null ? null : + EntityUtils.toString(entity, StandardCharsets.UTF_8); + } + + public ResponseValidator expectStatusCode(final Matcher<Integer> matcher) { + MatcherAssert.assertThat(response.getStatusLine().getStatusCode(), matcher); + return this; + } + + public ResponseValidator expectHeader(final String headerName, final Matcher<String> matcher) { + final Header firstHeader = response.getFirstHeader(headerName); + final String headerValue = firstHeader != null ? firstHeader.getValue() : null; + MatcherAssert.assertThat(headerValue, matcher); + assertThat(firstHeader, is(not(nullValue()))); + return this; + } + + public ResponseValidator expectNoHeader(final String headerName) { + final Header firstHeader = response.getFirstHeader(headerName); + assertThat(firstHeader, is(nullValue())); + return this; + } + + public ResponseValidator expectContent(final Matcher<String> matcher) throws IOException { + MatcherAssert.assertThat(content, matcher); + return this; + } + + public ResponseValidator expectTrailer(final String trailerName, final Matcher<String> matcher) { + // TODO: check trailer, not header + return expectHeader(trailerName, matcher); + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java new file mode 100644 index 00000000000..9d0aa02bbfe --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.ListenableFuture; +import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketListener; +import com.ning.http.client.websocket.WebSocketUpgradeHandler; + +import javax.net.ssl.SSLContext; +import java.io.IOException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class SimpleWebSocketClient { + + private final AsyncHttpClient client; + private final String scheme; + private final int listenPort; + + public SimpleWebSocketClient(final TestDriver driver) { + this(driver.newSslContext(), driver.server().getListenPort()); + } + + public SimpleWebSocketClient(final SSLContext sslContext, final int listenPort) { + final AsyncHttpClientConfig config = new AsyncHttpClientConfig.Builder().setSSLContext(sslContext).build(); + this.client = new AsyncHttpClient(new GrizzlyAsyncHttpProvider(config), config); + this.scheme = sslContext != null ? "wss" : "ws"; + this.listenPort = listenPort; + } + + public ListenableFuture<WebSocket> executeRequest(final String path, final WebSocketListener listener) + throws IOException { + return client.prepareGet(scheme + "://localhost:" + listenPort + path) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(listener).build()); + } + + public boolean close() { + client.close(); + return true; + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java new file mode 100644 index 00000000000..2f17d76a145 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java @@ -0,0 +1,84 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Key; +import com.google.inject.Module; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ssl.JKSKeyStore; +import com.yahoo.jdisc.http.ssl.SslContextFactory; +import com.yahoo.jdisc.http.ssl.SslKeyStore; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.nio.file.Paths; + +import static com.google.inject.name.Names.named; + +/** + * This class is based on the class by the same name in the jdisc_http_service module. + * It provides functionality for setting up a jdisc container with an HTTP server and handlers. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class TestDriver { + + private final com.yahoo.jdisc.test.TestDriver driver; + private final JettyHttpServer server; + private final SimpleHttpClient client; + + private TestDriver(com.yahoo.jdisc.test.TestDriver driver, JettyHttpServer server, SimpleHttpClient client) + throws IOException { + this.driver = driver; + this.server = server; + this.client = client; + } + + public static TestDriver newInstance(Class<? extends JettyHttpServer> serverClass, + RequestHandler requestHandler, + Module testConfig) throws IOException { + com.yahoo.jdisc.test.TestDriver driver = + com.yahoo.jdisc.test.TestDriver.newSimpleApplicationInstance(testConfig); + ContainerBuilder builder = driver.newContainerBuilder(); + JettyHttpServer server = builder.getInstance(serverClass); + builder.serverProviders().install(server); + builder.serverBindings().bind("*://*/*", requestHandler); + driver.activateContainer(builder); + server.start(); + + SimpleHttpClient client = new SimpleHttpClient(newSslContext(builder), server.getListenPort(), false); + return new TestDriver(driver, server, client); + } + + public boolean close() throws IOException { + server.close(); + server.release(); + return driver.close(); + } + + public JettyHttpServer server() { return server; } + + public SimpleHttpClient client() { return client; } + + public SimpleHttpClient newClient() throws IOException { return newClient(false); } + + public SimpleHttpClient newClient(final boolean useCompression) throws IOException { + return new SimpleHttpClient(newSslContext(), server.getListenPort(), useCompression); + } + + public SSLContext newSslContext() { + return newSslContext(driver.newContainerBuilder()); + } + + private static SSLContext newSslContext(final ContainerBuilder builder) { + ConnectorConfig.Ssl sslConfig = builder.getInstance(ConnectorConfig.class).ssl(); + if (!sslConfig.enabled()) return null; + + SslKeyStore keyStore = new JKSKeyStore(Paths.get(sslConfig.keyStorePath())); + keyStore.setKeyStorePassword(builder.getInstance(Key.get(String.class, named("keyStorePassword")))); + return SslContextFactory.newInstanceFromTrustStore(keyStore).getServerSSLContext(); + } + +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java new file mode 100644 index 00000000000..3137ae24b7b --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.http.guiceModules.ServletModule; +import com.yahoo.jdisc.http.server.FilterBindings; + +import java.io.IOException; + +import static com.google.inject.name.Names.named; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class TestDrivers { + + private static final String KEY_STORE = "src/test/resources/ssl_keystore_test.jks"; + public static final String KEY_STORE_PASSWORD = "secret"; + + public static TestDriver newConfiguredInstance(final RequestHandler requestHandler, + final ServerConfig.Builder serverConfig, + final ConnectorConfig.Builder connectorConfig, + final Module... guiceModules) throws IOException { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule(serverConfig, connectorConfig, guiceModules)); + } + + public static TestDriver newInstance(final RequestHandler requestHandler, + final Module... guiceModules) throws IOException { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule( + new ServerConfig.Builder(), + new ConnectorConfig.Builder(), + guiceModules + )); + } + + public static TestDriver newInstanceWithSsl(final RequestHandler requestHandler, + final Module... guiceModules) throws IOException { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule( + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .ssl(new ConnectorConfig.Ssl.Builder() + .enabled(true) + .keyDbKey("dummy-key-for-StaticKeyDbConnectorFactory.getPasswordFromKeydb") + .keyStorePath(KEY_STORE) + .trustStorePath(KEY_STORE)), + Modules.combine(new AbstractModule() { + + @Override + protected void configure() { + bind(String.class).annotatedWith(named("keyStorePassword")) + .toInstance(KEY_STORE_PASSWORD); + } + }, Modules.combine(guiceModules)) + )); + } + + private static Module newConfigModule( + final ServerConfig.Builder serverConfig, + final ConnectorConfig.Builder connectorConfigBuilder, + final Module... guiceModules) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(ServerConfig.class).toInstance(new ServerConfig(serverConfig)); + bind(ConnectorConfig.class).toInstance(new ConnectorConfig(connectorConfigBuilder)); + bind(FilterBindings.class).toInstance( + new FilterBindings( + new BindingRepository<RequestFilter>(), + new BindingRepository<ResponseFilter>())); + } + }, + new ConnectorFactoryRegistryModule(connectorConfigBuilder), + new ServletModule(), + Modules.combine(guiceModules)); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java new file mode 100644 index 00000000000..8a6ea28f2f9 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java @@ -0,0 +1,766 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.util.concurrent.SettableFuture; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketByteListener; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.http.server.FilterBindings; +import com.yahoo.jdisc.test.ServerProviderConformanceTest; +import org.glassfish.grizzly.websockets.HandshakeException; +import org.hamcrest.Matcher; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +//Ignore: Broken by jetty 9.2.{3,4} +class WebSocketServerConformanceTestIgnored extends ServerProviderConformanceTest { + + /* Some tests here are disabled. What they have in common is that the scenario + * involves waiting for an event (response write) in the request content channel's close() + * method, but Jetty will sometimes use the thread that is supposed to generate that event + * (the thread that writes the response) to deliver the close() notification, causing a + * deadlock. + * + * All in all, the WebSocket protocol doesn't map beautifully to JDisc APIs, which makes + * it hard to do proper testing here. Specifically, in order to cause the request content + * channel to be closed, we have to close the socket from the client side, which means + * that all bets are off regarding what response the client will see. So, the tests here + * that close the socket early can do no verification at all. However, it will be + * verified by the test framework that the server-side request processing finishes + * without any unexpected side effects. + */ + + @Override + @Test + public void testContainerNotReadyException() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testBindingSetNotFoundException() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testNoBindingSetSelectedException() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testBindingNotFoundException() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncCloseResponse() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncWriteResponse() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncHandleResponse() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithAsyncHandleResponse() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestException() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncCloseResponse() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncWriteResponse() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expectedError(instanceOf(HandshakeException.class)) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncCompletion() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicException() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncCompletion() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncCompletion() throws Throwable { + new TestRunner().expectResponseContent(is("myResponseContent")) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicException() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test(enabled = false) + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable { + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable { + new TestRunner().setCloseRequestEarly(true) + .execute(); + } + + @Override + @Test + public void testResponseWriteCompletionException() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testResponseCloseCompletionException() throws Throwable { + new TestRunner().execute(); + } + + @Override + @Test + public void testResponseCloseCompletionExceptionNoContent() throws Throwable { + new TestRunner().execute(); + } + + @SuppressWarnings("deprecation") + private class TestRunner implements Adapter<JettyHttpServer, WebSocketClient, Future<String>> { + + Matcher<String> expectedContent = null; + Matcher<Object> expectedError = null; + boolean closeRequestEarly; + + void execute() throws Throwable { + runTest(this); + } + + TestRunner expectResponseContent(final Matcher<String> matcher) { + assertThat(expectedError, is(nullValue())); + expectedContent = matcher; + return this; + } + + TestRunner expectedError(final Matcher<Object> matcher) { + assertThat(expectedContent, is(nullValue())); + expectedError = matcher; + return this; + } + + @Override + public Module newConfigModule() { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class) + .toInstance(new FilterBindings( + new BindingRepository<RequestFilter>(), + new BindingRepository<ResponseFilter>())); + bind(ServerConfig.class) + .toInstance(new ServerConfig(new ServerConfig.Builder())); + } + }, + new ConnectorFactoryRegistryModule()); + } + + @Override + public Class<JettyHttpServer> getServerProviderClass() { + return JettyHttpServer.class; + } + + @Override + public WebSocketClient newClient(final JettyHttpServer server) throws Throwable { + return new WebSocketClient(server.getListenPort()); + } + + @Override + public Future<String> executeRequest( + final WebSocketClient client, + final boolean withRequestContent) throws Throwable { + final String requestContent = withRequestContent ? "myRequestContent" : null; + return client.executeRequest(requestContent, closeRequestEarly); + } + + @Override + public Iterable<ByteBuffer> newResponseContent() { + return Collections.singleton(StandardCharsets.UTF_8.encode("myResponseContent")); + } + + @Override + public void validateResponse(final Future<String> responseFuture) throws Throwable { + String content = null; + Throwable error = null; + try { + content = responseFuture.get(60, TimeUnit.SECONDS); + } catch (final ExecutionException e) { + error = e.getCause(); + } + if (expectedContent != null) { + assertThat(content, expectedContent); + } + if (expectedError != null) { + assertThat(error, expectedError); + } + } + + public TestRunner setCloseRequestEarly(final boolean closeRequestEarly) { + this.closeRequestEarly = closeRequestEarly; + return this; + } + } + + private static class WebSocketClient implements Closeable { + + final SimpleWebSocketClient delegate; + + WebSocketClient(final int listenPort) { + delegate = new SimpleWebSocketClient(null, listenPort); + } + + Future<String> executeRequest(final String requestContent, final boolean closeRequest) throws Exception { + final MyWebSocketListener listener = new MyWebSocketListener(requestContent, closeRequest); + delegate.executeRequest("/status.html", listener); + return listener.response; + } + + @Override + public void close() throws IOException { + delegate.close(); + } + } + + // You may find this class slightly ugly, with all the logic to do closing in various places. + // The reason this is necessary is the combination of several things: + // 1) The way WebSocket is implemented in JDisc and mapped to JDisc APIs, specifically: + // - When the client closes a socket, it is not guaranteed to receive anything more from the server + // (the protocol could support it, but neither the client nor server library that we use do) + // - The server won't close a socket until the client does, but by then it is too late for the + // server to send responses. + // 2) The conformance test framework is designed mostly for request-response protocols. It assumes that + // it is self-evident when communication is over, and only _then_ moves on to validating the response. + // + // The problem is that we cannot close the socket right after sending the request, as we are then + // not guaranteed to receive response data (nondeterministic behavior). We cannot close the socket when + // the conformance test framework asks us to validate the response, because we'd never get to that + // - the request processing isn't finished until some party closes the socket! So how do we decide when + // to close the socket? Well, what any "real" client would do is close it when we're satisfied with the + // response. And these tests never return anything more than a single response message, so if we get one, + // we consider ourselves done. Also if we get an error. + private static class MyWebSocketListener implements WebSocketByteListener { + + // This is used to temporarily concatenate response fragments until we have a complete response message. + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // This is used to signal that we have received a response, which may be data or an error. + // This is attempted set from multiple code locations, but the first one "wins" (the others are ignored). + final SettableFuture<String> response = SettableFuture.create(); + + // If this is true, the client will close the socket immediately after sending the request content. + // This means that there is no guarantee that the client will receive any response from the server. + // If this is false, the socket is closed after receiving a response from the server. Since the server + // response in theory can be infinitely long, we define "receive a response" as receiving a single message, + // since that is what is sent from the server in these tests. + final boolean closeEarly; + + final byte[] requestContent; + + // We need to be able to close the WebSocket in methods that are not handed the WebSocket instance. + // We use this to keep a reference to it. + private final AtomicReference<WebSocket> webSocketRef = new AtomicReference<>(null); + + MyWebSocketListener(final String requestContent, final boolean closeEarly) { + this.closeEarly = closeEarly; + this.requestContent = requestContent != null ? requestContent.getBytes(StandardCharsets.UTF_8) : null; + } + + @Override + public void onOpen(final WebSocket webSocket) { + this.webSocketRef.set(webSocket); + if (requestContent != null) { + webSocket.sendMessage(requestContent); + } + if (closeEarly) { + webSocket.close(); + } + } + + @Override + public void onClose(final WebSocket webSocket) { + response.set(""); + this.webSocketRef.set(null); + } + + @Override + public void onError(final Throwable t) { + response.setException(t); + closeSocket(); + } + + @Override + public void onMessage(final byte[] buf) { + final String message = new String(buf, StandardCharsets.UTF_8); + response.set(message); + closeSocket(); + } + + @Override + public void onFragment(final byte[] buf, final boolean last) { + try { + out.write(buf); + if (last) { + response.set(new String(out.toByteArray(), StandardCharsets.UTF_8)); + closeSocket(); + } + } catch (final IOException e) { + response.setException(e); + } + } + + private void closeSocket() { + final WebSocket webSocket = webSocketRef.get(); + if (webSocket != null) { + webSocket.close(); + } + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java new file mode 100644 index 00000000000..e2f94a1949d --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java @@ -0,0 +1,102 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.util.concurrent.SettableFuture; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketByteListener; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.jdisc.Response.Status.OK; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class WebSocketServerTest { + + @Test(enabled = false) + public void requireThatServerCanRespondToRequest() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + final SimpleWebSocketClient client = new SimpleWebSocketClient(driver); + final MyWebSocketListener listener = new MyWebSocketListener("Hello World!"); + client.executeRequest("/status.html", listener); + assertThat(listener.response.get(60, TimeUnit.SECONDS), is("Hello World!")); + assertThat(client.close(), is(true)); + assertThat(driver.close(), is(true)); + } + + //@Test Ignored: Broken in jetty 9.2.{3,4} + public void requireThatServerCanRespondToSslRequest() throws Exception { + final TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler()); + final SimpleWebSocketClient client = new SimpleWebSocketClient(driver); + final MyWebSocketListener listener = new MyWebSocketListener("Hello World!"); + client.executeRequest("/status.html", listener); + assertThat(listener.response.get(60, TimeUnit.SECONDS), is("Hello World!")); + assertThat(client.close(), is(true)); + assertThat(driver.close(), is(true)); + } + + private static class EchoRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return handler.handleResponse(new Response(OK)); + } + } + + private static class MyWebSocketListener implements WebSocketByteListener { + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final SettableFuture<String> response = SettableFuture.create(); + final byte[] requestContent; + + MyWebSocketListener(final String requestContent) { + this.requestContent = requestContent.getBytes(StandardCharsets.UTF_8); + } + + @Override + public void onOpen(final WebSocket webSocket) { + webSocket.sendMessage(requestContent); + webSocket.close(); + } + + @Override + public void onClose(final WebSocket webSocket) { + response.set(new String(out.toByteArray(), StandardCharsets.UTF_8)); + } + + @Override + public void onError(final Throwable t) { + response.setException(t); + } + + @Override + public void onMessage(final byte[] buf) { + try { + out.write(buf); + } catch (final IOException e) { + response.setException(e); + } + } + + @Override + public void onFragment(final byte[] buf, final boolean last) { + try { + out.write(buf); + } catch (final IOException e) { + response.setException(e); + } + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java new file mode 100644 index 00000000000..b07499d0e02 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java @@ -0,0 +1,175 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.server.FilterBindings; +import com.yahoo.jdisc.http.server.jetty.FilterInvoker; +import com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; + +/** + * @author tonytv + */ +public class JDiscFilterForServletTest extends ServletTestBase { + @Test + public void request_filter_can_return_response() throws IOException, InterruptedException { + TestDriver testDriver = requestFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH).execute(); + + response.expectContent(containsString(TestRequestFilter.responseContent)); + } + + @Test + public void request_can_be_forwarded_through_request_filter_to_servlet() throws IOException { + TestDriver testDriver = requestFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH). + addHeader(TestRequestFilter.BYPASS_FILTER_HEADER, Boolean.TRUE.toString()). + execute(); + + response.expectContent(containsString(TestServlet.RESPONSE_CONTENT)); + } + + @Test + public void response_filter_can_modify_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH).execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + @Test + public void response_filter_is_run_on_empty_sync_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, NoContentTestServlet.PATH).execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + @Test + public void response_filter_is_run_on_empty_async_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, NoContentTestServlet.PATH). + addHeader(NoContentTestServlet.HEADER_ASYNC, Boolean.TRUE.toString()). + execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + private TestDriver requestFilterTestDriver() throws IOException { + return TestDrivers.newInstance(dummyRequestHandler, bindings(requestFilters(), noBindings())); + } + + private TestDriver responseFilterTestDriver() throws IOException { + return TestDrivers.newInstance(dummyRequestHandler, bindings(noBindings(), responseFilters())); + } + + private Module bindings(BindingRepository<RequestFilter> requestFilters, + BindingRepository<ResponseFilter> responseFilters) { + + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class).toInstance(new FilterBindings(requestFilters, responseFilters)); + bind(FilterInvoker.class).toInstance(new FilterInvoker() { + @Override + public HttpServletRequest invokeRequestFilterChain( + RequestFilter requestFilter, + URI uri, + HttpServletRequest httpRequest, + ResponseHandler responseHandler) { + TestRequestFilter filter = (TestRequestFilter) requestFilter; + filter.runAsSecurityFilter(httpRequest, responseHandler); + return httpRequest; + } + + @Override + public void invokeResponseFilterChain( + ResponseFilter responseFilter, + URI uri, + HttpServletRequest request, + HttpServletResponse response) { + + TestResponseFilter filter = (TestResponseFilter) responseFilter; + filter.runAsSecurityFilter(request, response); + } + }); + } + }, + guiceModule()); + } + + private BindingRepository<RequestFilter> requestFilters() { + BindingRepository<RequestFilter> repository = new BindingRepository<>(); + repository.bind("http://*/*" , new TestRequestFilter()); + return repository; + } + + private BindingRepository<ResponseFilter> responseFilters() { + BindingRepository<ResponseFilter> repository = new BindingRepository<>(); + repository.bind("http://*/*" , new TestResponseFilter()); + return repository; + } + + private <T> BindingRepository<T> noBindings() { + return new BindingRepository<>(); + } + + + static class TestRequestFilter extends AbstractResource implements RequestFilter { + static final String simpleName = TestRequestFilter.class.getSimpleName(); + static final String responseContent = "Rejected by " + simpleName; + static final String BYPASS_FILTER_HEADER = "BYPASS_HEADER" + simpleName; + + @Override + public void filter(HttpRequest request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + public void runAsSecurityFilter(HttpServletRequest request, ResponseHandler responseHandler) { + if (Boolean.parseBoolean(request.getHeader(BYPASS_FILTER_HEADER))) + return; + + ContentChannel contentChannel = responseHandler.handleResponse(new Response(500)); + contentChannel.write(ByteBuffer.wrap(responseContent.getBytes(StandardCharsets.UTF_8)), null); + contentChannel.close(null); + } + } + + + static class TestResponseFilter extends AbstractResource implements ResponseFilter { + static final String INVOKED_HEADER = TestResponseFilter.class.getSimpleName() + "_INVOKED_HEADER"; + + @Override + public void filter(Response response, Request request) { + throw new UnsupportedClassVersionError(); + } + + public void runAsSecurityFilter(HttpServletRequest request, HttpServletResponse response) { + response.addHeader(INVOKED_HEADER, Boolean.TRUE.toString()); + } + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java new file mode 100644 index 00000000000..1aede0e9850 --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java @@ -0,0 +1,62 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import org.mockito.verification.VerificationMode; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +/** + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class ServletAccessLoggingTest extends ServletTestBase { + private static final int MAX_LOG_WAIT_TIME_MILLIS = (int) TimeUnit.SECONDS.toMillis(60); + + @Test + public void accessLogIsInvokedForNonJDiscServlet() throws Exception { + final AccessLog accessLog = mock(AccessLog.class); + final TestDriver testDriver = newTestDriver(accessLog); + httpGet(testDriver, TestServlet.PATH).execute(); + verifyCallsLog(accessLog, timeout(MAX_LOG_WAIT_TIME_MILLIS).times(1)); + } + + @Test + public void accessLogIsInvokedForJDiscServlet() throws Exception { + final AccessLog accessLog = mock(AccessLog.class); + final TestDriver testDriver = newTestDriver(accessLog); + testDriver.client().newGet("/status.html").execute(); + verifyCallsLog(accessLog, timeout(MAX_LOG_WAIT_TIME_MILLIS).times(1)); + } + + private void verifyCallsLog(final AccessLog accessLog, final VerificationMode verificationMode) { + verify(accessLog, verificationMode).log(any(AccessLogEntry.class)); + } + + private TestDriver newTestDriver(final AccessLog accessLog) throws IOException { + return TestDrivers.newInstance(dummyRequestHandler, bindings(accessLog)); + } + + private Module bindings(final AccessLog accessLog) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(AccessLog.class).toInstance(accessLog); + } + }, + guiceModule()); + } +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java new file mode 100644 index 00000000000..d427842c30e --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java @@ -0,0 +1,123 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.ServletPathsConfig.Servlets.Builder; +import com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.RequestExecutor; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.jetty.servlet.ServletHolder; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author tonytv + * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a> + */ +public class ServletTestBase { + private ImmutableMap<Pair<ComponentId, String>, HttpServlet> servlets = ImmutableMap.of( + ImmutablePair.of(TestServlet.ID, TestServlet.PATH), new TestServlet(), + ImmutablePair.of(NoContentTestServlet.ID, NoContentTestServlet.PATH), new NoContentTestServlet()); + + protected RequestExecutor httpGet(TestDriver testDriver, String path) { + return testDriver.client().newGet("/" + path); + } + + protected ServletPathsConfig createServletPathConfig() { + ServletPathsConfig.Builder configBuilder = new ServletPathsConfig.Builder(); + + servlets.forEach((idAndPath, servlet) -> + configBuilder.servlets( + idAndPath.getLeft().stringValue(), + new Builder().path(idAndPath.getRight()))); + + return new ServletPathsConfig(configBuilder); + } + + protected ComponentRegistry<ServletHolder> servlets() { + ComponentRegistry<ServletHolder> result = new ComponentRegistry<>(); + + servlets.forEach((idAndPath, servlet) -> + result.register(idAndPath.getLeft(), new ServletHolder(servlet))); + + result.freeze(); + return result; + } + + protected Module guiceModule() { + return new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral<ComponentRegistry<ServletHolder>>(){}).toInstance(servlets()); + bind(ServletPathsConfig.class).toInstance(createServletPathConfig()); + } + }; + } + + protected static class TestServlet extends HttpServlet { + static final String PATH = "servlet/test-servlet"; + static final ComponentId ID = ComponentId.fromString("test-servlet"); + static final String RESPONSE_CONTENT = "Response from " + TestServlet.class.getSimpleName(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + PrintWriter writer = response.getWriter(); + writer.write(RESPONSE_CONTENT); + writer.close(); + } + } + + @WebServlet(asyncSupported = true) + protected static class NoContentTestServlet extends HttpServlet { + static final String HEADER_ASYNC = "HEADER_ASYNC"; + + static final String PATH = "servlet/no-content-test-servlet"; + static final ComponentId ID = ComponentId.fromString("no-content-test-servlet"); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (request.getHeader(HEADER_ASYNC) != null) { + asyncGet(request); + } + } + + private void asyncGet(HttpServletRequest request) { + request.startAsync().start(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + log("Interrupted", e); + } finally { + request.getAsyncContext().complete(); + } + }); + } + } + + + protected static final RequestHandler dummyRequestHandler = new AbstractRequestHandler() { + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + }; +} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java new file mode 100644 index 00000000000..3693114dfdb --- /dev/null +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.test; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.TimeUnit; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RemoteServerTestCase { + + @Test(enabled = false) + public void requireThatRequestUriFactoryWorks() throws IOException { + RemoteServer server = RemoteServer.newInstance(); + try { + server.newRequestUri((String)null); + fail(); + } catch (NullPointerException e) { + + } + try { + server.newRequestUri((URI)null); + fail(); + } catch (NullPointerException e) { + + } + try { + server.newRequestUri("foo"); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getCause() instanceof URISyntaxException); + } + URI requestUri = server.newRequestUri("/foo?baz=cox#bar"); + URI serverUri = server.connectionSpec(); + assertEquals(serverUri.getScheme(), requestUri.getScheme()); + assertEquals(serverUri.getUserInfo(), requestUri.getUserInfo()); + assertEquals(serverUri.getHost(), requestUri.getHost()); + assertEquals(serverUri.getPort(), requestUri.getPort()); + assertEquals("/foo", requestUri.getPath()); + assertEquals("baz=cox", requestUri.getQuery()); + assertEquals("bar", requestUri.getFragment()); + assertTrue(server.close(60, TimeUnit.SECONDS)); + } +} diff --git a/jdisc_http_service/src/test/resources/ssl_keystore_test.jks b/jdisc_http_service/src/test/resources/ssl_keystore_test.jks Binary files differnew file mode 100644 index 00000000000..6dbb19b9692 --- /dev/null +++ b/jdisc_http_service/src/test/resources/ssl_keystore_test.jks |