I've been following this post on Medium to learn how to create and run a dotnet core console app in a docker container, and post to a dotnet core API in another container.
When I run the two applications side-by-side (without docker, i.e. just debugging in vscode), everything works OK - the console app can post to the API. However, when I run the applications in containers using docker-compose up --build, I get an error when the application tries to post to the api:
Unhandled exception. System.AggregateException: One or more errors occurred. (The SSL connection could not be established, see inner exception.)
System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
System.IO.IOException: The handshake failed due to an unexpected packet format.
Searching for solutions to this error hasn't helped much, and I feel that the problem may simply be connectivity between the two containers, but I've had no luck trying to resolve it.
My docker-compose file is as follows:
version: '3.4'
services:
publisher_api:
image: my_publisher_api:latest
container_name: my_publisher_api_container
build:
context: ./publisher_api
dockerfile: Dockerfile
worker:
image: my_worker
container_name: my_worker_container
depends_on:
- "publisher_api"
build:
context: ./worker
dockerfile: Dockerfile
My console app code (or at least the relevant part) is:
public static async Task PostMessage(object postData)
{
var json = JsonConvert.SerializeObject(postData);
var content = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
using (var httpClientHandler = new HttpClientHandler())
{
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; };
using (var client = new HttpClient(httpClientHandler))
{
var result = await client.PostAsync("https://my_publisher_api_container:80/values", content);
string resultContent = await result.Content.ReadAsStringAsync();
Console.WriteLine($"Server returned {resultContent}");
}
}
}
I wont post any of the API code, as I dont think any of it should be relevant, but please let me know if you think it would help.
If anyone has any idea on what the cause of this error is or how to resolve it, I'd appreciate the help.
Edit
Thought it would be useful to include the versions being used:
dotnet core: 3.0.101
docker: 19.03.5, build 633a0ea838
Looks like I had mad a couple of fairly obvious mistakes, however they're not so obvious when you're completely new to Docker, like me.
The hostname to post to should be the name of the service, not the container.In my case, I had to change the console app to post to the name of the API service declared in the docker-compose file, publisher_api.
Use HTTP instead of HTTPS. When I debugged the API locally, it launches with HTTPS by default. I assumed I would use HTTPS when running the container in docker, but this doesn't seem to work by default. Changing to HTTP resolved the issue (although this ideally will be a short-term solution).
So just for completeness, here's my updated code. Only the URL that the console app posts to had to change:
public static async Task PostMessage(object postData)
{
var json = JsonConvert.SerializeObject(postData);
var content = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
using (var httpClientHandler = new HttpClientHandler())
{
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; };
using (var client = new HttpClient(httpClientHandler))
{
var result = await client.PostAsync("http://publisher_api80/values", content);
string resultContent = await result.Content.ReadAsStringAsync();
Console.WriteLine($"Server returned {resultContent}");
}
}
}
Related
hi i am new to identity server 4. recently i made simple small project using identity server 4. solution has 3 projects of 1.identityserver4 2.api resource 3.client razor. every thing fine in localhost. but when i try the same project in docker i got problem with identityserver 4. this is the repository of the test project here
Problem
when web app client is started it should redirect to identity server 4 login page which happens with local host. but here i ran into the problem.
NOTE: until now according to searching i reach there may be certificate problem OR networking and DNS issue and i tried some suggested approaches but it wont work or maybe i doing it wrong
1st i tried set the authority address to the container name with port like http://identityserver4:9001 (also for client address which is port 5001) which throws the errors as
SocketException: Connection refused
System.Net.Sockets.Socket+AwaitableSocketAsyncEventArgs.ThrowException(SocketError
error, CancellationToken cancellationToken)
HttpRequestException: Connection refused (identityserver4:9001)
System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(string host,
int port, HttpRequestMessage initialRequest, bool async,
CancellationToken cancellationToken)
IOException: IDX20804: Unable to retrieve document from:
'http://identityserver4:9001/.well-known/openid-configuration'.
Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(string
address, CancellationToken cancel)
InvalidOperationException: IDX20803: Unable to obtain configuration
from: 'http://identityserver4:9001/.well-known/openid-configuration'.
Microsoft.IdentityModel.Protocols.ConfigurationManager.GetConfigurationAsync(CancellationToken
cancel)
In this case the error came from client app.
When i shutdown the identityserver4 service and try the client it throws the same error. so it seems totally it could not reach identity server.
i tried the defined bridge network in docker-compose as well and i got the same result
also i tried http request to http://identityserver4:9001/.well-known/openid-configuration from client (i removed the client authorization & authentication stuff for test this) which throws the Connection refused error in response.
2nd i tried set the authority address to http://identityserver4:80. in this case the client app redirect to identity server 4 identityserver4/connect/authorize but the browser throws
dial tcp: lookup identityserver4 on 127.0.0.11:53: no such host
in this case the client passed and redirect to identity server 4
http request to http://identityserver4:80/.well-known/openid-configuration from client (i removed the client authorization & authentication stuff for test this) gives the correct response.
i linked the github repository above but for ease i also added mu configuration as below.
i try some approaches but this is the BASE CODE from the first attempt.
docker-compose.override.yml
version: '3.4'
services:
identityserver4:
container_name: identityserver4
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "9001:80"
networks:
- mynet
clientapp:
container_name: clientapp
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "5001:80"
depends_on:
- identityserver4
networks:
- mynet
apiresource:
container_name: apiresource
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "8001:80"
networks:
- mynet
networks:
mynet:
external: true
Identity Server
client configuration in Config.cs
new Client
{
ClientId = "razorClient",
ClientName = "RAZOR Client App",
AllowedGrantTypes= GrantTypes.Hybrid,
RequirePkce = false,
AllowRememberConsent = false,
RedirectUris = new List<string>()
{
"http://clientapp:80/signin-oidc"
},
PostLogoutRedirectUris = new List<string>()
{
"http://clientapp:80/signout-callback-oidc"
},
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>()
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"MYAPI"
},
AllowAccessTokensViaBrowser = true,
}
program.cs
using IdentityServer;
var builder = WebApplication.CreateBuilder(args);
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
builder.Services.AddControllersWithViews();
builder.Services.AddIdentityServer(options =>
{
options.IssuerUri = "http://identityserver4:80";
})
.AddInMemoryClients(Config.Clients)
.AddInMemoryIdentityResources(Config.IdentityResources)
//.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddTestUsers(Config.TestUsers)
.AddDeveloperSigningCredential();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.Run();
Client APP
program.cs
using ClientApp.Data;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Mvc.Authorization;
var builder = WebApplication.CreateBuilder(args);
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
builder.Services.AddHttpClient();
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = "http://identityserver4:80";
options.MetadataAddress = "http://identityserver4:80/.well-known/openid-configuration";
//options.BackchannelHttpHandler = new HttpClientHandler
//{
// ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
//};
options.RequireHttpsMetadata = false;
options.ClientId = "razorClient";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("MYAPI");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
builder.Services.AddMvcCore(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHttpsRedirection();
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
i got the solution
Description
the problem is the container communication. when running the project on localhost (not on docker) every thing is set to localhost:port address including authority address of is4. the app is authorized or redirect to login page simply. but in docker env the authority have to addressed based on docker inter-communication address ( local host is unknown here which leads to connection refused ) which is known by docker containers network but not machine localhost. so in that case the authority of is4 works but when it comes to redirect to login page ( identity server 4 UI ) based on authority address that address is unknown by local machine so it throws DNS error. so according to #Dimitris Maragkos answer i set the base addresses to local machine Ip address like
options.Authority= "http://192.168.8.202:9001"
including the client and api relevant required addresses.
I'm new to Docker and RabbitMQ and I've been trying for 2 days to solve an error in my docker containers which contains: api_client, api_consumer, RabbitMQ. I've done a research and tried to read as many threads with this problem as I found but unfortunately nothing helped.
So here is my code:
compose.yml
services:
api_client:
build:
context: ""
dockerfile: apps/api_client/Dockerfile
env_file:
- ./config/.env.local
restart: always
ports:
- "3000:3000"
depends_on:
- rabbitmq
api_consumer:
build:
context: ""
dockerfile: apps/api_consumer/Dockerfile
env_file:
- ./config/.env.local
restart: always
depends_on:
- rabbitmq
rabbitmq:
image: rabbitmq:3.9.2-management
container_name: rabbitmq
hostname: rabbitmq
volumes:
- /var/lib/rabbitmq
- ./rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf
ports:
- "5672:5672"
- "15672:15672"
main.ts (in api_consumer)
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
ApiConsumerModule,
{
transport: Transport.RMQ,
options: {
queue: 'test_queue',
urls: ['amqp://guest:guest#rabbitmq:5672'],
queueOptions: {
durable: true
}
}
},
);
const AWSAppConfig = app.get(AwsAppconfigLoaderService);
const Log = new Logger(ApiClientService.name);
await AWSAppConfig.loadAWSAppConfig()
.then((_) => {
Log.log(AWSAppConfig.getAppName());
})
.catch((err) => {
Log.error(
`Error occured while downloading AWS Config: ${JSON.stringify(
err,
)}`,
);
});
await app.listen();
}
bootstrap();
api-client.module.ts (in api_client)
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [AppConfig],
}),
ClientsModule.register([{
name: GET_MATCHED_DEVICES,
transport: Transport.RMQ,
options: {
queue: 'test_queue',
urls: ['amqp://guest:guest#rabbitmq:5672'],
queueOptions: {
durable: true
}
}
},
]),
AwsAppconfigLoaderModule,
],
controllers: [ApiClientController],
providers: [ApiClientService],
})
export class ApiClientModule {}
Functionality is simple- when GET on localhost:3000 (api_client) is called, it calls (in controller) return this.client.send('getSample', "hello") and then in api_consumer it should call (in controller)
#MessagePattern('getSample')
getSample(data): string {
Logger.debug(data)
return "It works!";
}
When all docker services start there is the first error:
Disconnected from RMQ. Trying to reconnect.
{
"err": {
"code": 406,
"classId": 60,
"methodId": 40
}
}
And then when I try to access the localhost:3000, this error always occur:
Error: Channel closed by server: 406 (PRECONDITION-FAILED) with message "PRECONDITION_FAILED - fast reply consumer does not exist"
Both errors come from api_client.
What I've tried and didn't help:
-change durable to false or remove durable options completely
-add noAck
-remove queue in adminer on localhost:15672 (which works fine)
-remove port from urls in both microservices
-as you can see the queue options are the same in both microservices
Now the most absurd thing is that this code did work absolutely fine until I started to work on second compose file (and dockerfiles) for local (faster) development with volumes. Then suddenly these errors have started to occur and even if I undid all my code changes the errors are still there. Because of this I've wiped all my volumes (with docker system prune -a --volumes) many times but still nothing. My OS is Ubuntu 20.04
I am completely out of ideas so I've written it here in hope for some help, please.
The failure occurs because the app can not connect to rabbitmq, the problem resides in your docker compose. Make sure the services are using the same network in docker and are able to communicate.
I know it sounds strange but I have had the same problem yesterday, also using a docker-compose file and without making any changes to it or to the rabbitmq logic, it broke. I tried many things, and when i changed
return this.client.send('getSample', "hello") and #MessagePattern
to
return this.client.emit('getSample', "hello") and #EventPattern
thinking it wouldn't make sense for it to fix the issue, it actually did.
I suggest you try that and tell me if it works, sorry if i can't help you more.
So after 5 days of figuring out what causes the error I've found out the problem lies in Nest itself. The bug has been documented here. I've deleted all the code associated with #nestjs/microservices and tried to use this approach using amqlib only and the request/response functionality finally works.
I am trying to use redis with tls with a netcore application and I get an authentication error
The Setup:
Docker:
I created a redis docker container using redis:6.2.0
docker-compose.yaml:
.
.
redis:
image: redis:6.2.0
command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes
container_name: "cxm-redis"
ports:
- "6379:6379"
volumes:
- cxm-redis-data:/data
- C:/SaaS/certs/redis.conf:/usr/local/etc/redis/redis.conf
- C:/SaaS/certs/tests/tls/redis.crt:/usr/local/etc/redis/redis.crt
- C:/SaaS/certs/tests/tls/redis.key:/usr/local/etc/redis/redis.key
- C:/SaaS/certs/tests/tls/ca.crt:/usr/local/etc/redis/ca.crt
up to here all looks good, (as far as I can tell) I managed to authenticate using the following command
redis-cli --tls --cert ../usr/local/etc/redis/redis.crt --key /usr/local/etc/redis/redis.key --cacert /usr/local/etc/redis/ca.crt and I can succesfully ping and request keys.
I created the certificates with openssl and for the redis.conf i am using the redis.conf example from redis
The important bits:
### TLS
tls-port 6379
tls-cert-file /usr/local/etc/redis/redis.crt
tls-key-file /usr/local/etc/redis/redis.key
tls-ca-cert-file /usr/local/etc/redis/ca.crt
netcore:
For my .netcore application I am using the StackExchange library and for the TLS connection I followed the instructions here, like so
var options = new ConfigurationOptions
{
EndPoints = { "redis-test:6379" },
Password = "not-the-actual-password",
Ssl = true
};
options.CertificateSelection += delegate {
return new X509Certificate2("./redis_certificate.p12");
};
_db = ConnectionMultiplexer.Connect(options).GetDatabase();
the redis_certificate.p12 was generated using openssl with this command line
openssl pkcs12 -export -out sample_certificate.p12 -inkey redis.key -in redis.crt
The Issue:
When I make a request to redis from my app I get the following error:
It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly. AuthenticationFailure on redis-test:6379/Interactive, Initializing/NotStarted
in my apps logs, and I get the following in my redis logs:
Error accepting a client connection: error:1408F10B:SSL routines:ssl3_get_record:wrong version number
Error accepting a client connection: error:14094418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca,
Error accepting a client connection: (null)
Are there any apparent mistakes in my setup I am failing to see? This is my first time trying this and maybe I am assuming too much or heading down the wrong way..
Trying to resolve this I found several questions with a similar issue but implementing their fixes did not resolve my issue..
a few of the things I tried
sending different ssl protocols from my .netcore app
sending the pfx/p12 certificate in different ways
several different redis configurations
Edit: I can provide as much code as needed!
For any one facing the same issue, it seems the server was using a non routed CA for the server certificates, the solution I found was to use the CertificateValidation callback of StackExchange.Redis library with the following code
private static bool CheckServerCertificate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) == SslPolicyErrors.RemoteCertificateChainErrors)
{
// check that the untrusted ca is in the chain
var ca = new X509Certificate2(_redisSettings.CertificatePath);
var caFound = chain.ChainElements
.Cast<X509ChainElement>()
.Any(x => x.Certificate.Thumbprint == ca.Thumbprint);
return caFound;
}
return false;
}
also an important part of the code being the condition
if((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) == SslPolicyErrors.RemoteCertificateChainErrors)
Setup
Trying to run chrome headless as a container (Image: https://hub.docker.com/r/alpeware/chrome-headless-trunk) in my docker-compose and connecting to it from another container.
Problem
To actually connect to chrome inside the container, I first need to retrieve the webSocketDebuggerUrl, which is available at http://0.0.0.0:9222/json/version of the chrome-headless.
The Problem is: my request to this path always fails with
RequestError: Error: connect ECONNREFUSED 0.0.0.0:9222
and cant get the webSocketDebuggerUrl to connect to chrome.
Some more Info
Also if I visit http://0.0.0.0:9222/json/version in my browser myself, copy the url and hardcode it into my puppeteer.connect(), it ONLY works as expected, if I replace the address of '0.0.0.0' to my (linked) container-name (specified in docker-compose): http://chrome:9222/json/version
If I try to request the webSocketDebuggerUrl from /json/version while using container-name address (http://chrome:9222/json/version) i get the error
StatusCodeError: 500 - "Host header is specified and is not an IP address or localhost."
My Code (abstraction)
const rp = require('request-promise')
const puppeteer = require('puppeteer-core')
let url = await rp({uri:'http://0.0.0.0:9222/json/version', json: true }).then(res => res.webSocketDebuggerUrl)
let browser = await puppeteer.connect({ browserWSEndpoint: url })
Well, since the errorMessage from the 500 said "host is specified", ist just set that header to empty, and now I can successfully request the webSocketDebuggerUrl.
The solution feels a bit hacky, so if anyone has a suggestion on how to improve it I'd be happy:
const puppeteer = require('puppeteer-core')
const rp = require('request-promise')
let websocket = await rp({uri:'http://chrome:9222/json/version', json: true, headers: {'Host': ''} })
.then(res => res.webSocketDebuggerUrl.replace('ws://','ws://chrome:9222'))
let browser = await puppeteer.connect({ browserWSEndpoint: websocket })
I am trying to submit a transaction to Hyperledger Sawtooth v1.0.1 using javascript to a validator running on localhost. The code for the post request is as below:
request.post({
url: constants.API_URL + '/batches',
body: batchListBytes,
headers: { 'Content-Type': 'application/octet-stream' }
}, (err, response) => {
if (err) {
console.log(err);
return cb(err)
}
console.log(response.body);
return cb(null, response.body);
});
The transaction gets processed when submitted from an backend nodejs application, but it returns an OPTIONS http://localhost:8080/batches 405 (Method Not Allowed) error when submitted from client. These are the options that I have tried:
Inject Access-Control-Allow-* headers into the response using an extension: The response still gives the same error
Remove the custom header to bypass preflight request: This makes the validator throw an error as shown:
...
sawtooth-rest-api-default | KeyError: "Key not found: 'Content-Type'"
sawtooth-rest-api-default | [2018-03-15 08:07:37.670 ERROR web_protocol] Error handling request
sawtooth-rest-api-default | Traceback (most recent call last):
...
The unmodified POST request from the browser gets the following response headers from the validator:
HTTP/1.1 405 Method Not Allowed
Content-Type: text/plain; charset=utf-8
Allow: GET,HEAD,POST
Content-Length: 23
Date: Thu, 15 Mar 2018 08:42:01 GMT
Server: Python/3.5 aiohttp/2.3.2
So, I guess OPTIONS method is not handled in the validator. A GET request for the state goes through fine when the CORS headers are added. This issue was also not faced in Sawtooth v0.8.
I am using docker to start the validator, and the commands to start it are a slightly modified version of those given in the LinuxFoundationX: LFS171x course. The relevant commands are below:
bash -c \"\
sawadm keygen && \
sawtooth keygen my_key && \
sawset genesis -k /root/.sawtooth/keys/my_key.priv && \
sawadm genesis config-genesis.batch && \
sawtooth-validator -vv \
--endpoint tcp://validator:8800 \
--bind component:tcp://eth0:4004 \
--bind network:tcp://eth0:8800
Can someone please guide me as to how to solve this problem?
CORS issues are always the best.
What is CORS?
Your browser trying to protect users from bring directed to a page they think is the frontend for an API, but is actually fraudulent. Anytime a web page tries to access an API on a different domain, that API will need to explicitly give the webpage permission, or the browser will block the request. This is why you can query the API from Node.js (no browser), and can put the REST API address directly into your address bar (same domain). However, trying to go from localhost:3000 to localhost:8008 or from file://path/to/your/index.html to localhost:8008 is going to get blocked.
Why doesn't the Sawtooth REST API handle OPTIONS requests?
The Sawtooth REST API does not know the domain you are going to run your web page from, so it can't whitelist it explicitly. It is possible to whitelist all domains, but this obviously destroys any protection CORS might give you. Rather than try to weigh the costs and benefits of this approach for all Sawtooth users everywhere, the decision was made to make the REST API as lightweight and security agnostic as possible. Any developer using it would be expected to put it behind a proxy server, and they can make whatever security decisions they need on that proxy layer.
So how do you fix it?
You need to setup a proxy server that will put the REST API and your web page on the same domain. There is no quick configuration option for this. You will have to set up an actual server. Obviously there are lots of ways to do this. If you are already familiar with Node, you could serve the page from Node.js, and then have the Node server proxy the API calls. If you are already running all of the Sawtooth components with docker-compose though, it might be easier to use Docker and Apache.
Setting up an Apache Proxy with Docker
Create your Dockerfile
In the same directory as your web app create a text file called "Dockerfile" (no extension). Then make it look like this:
FROM httpd:2.4
RUN echo "\
LoadModule proxy_module modules/mod_proxy.so\n\
LoadModule proxy_http_module modules/mod_proxy_http.so\n\
ProxyPass /api http://rest-api:8008\n\
ProxyPassReverse /api http://rest-api:8008\n\
RequestHeader set X-Forwarded-Path \"/api\"\n\
" >>/usr/local/apache2/conf/httpd.conf
This is going to do a couple of things. First it will pull down the httpd module from DockerHub, which is just a simple static server. Then we are using a bit of bash to add five lines to Apache's configuration file. These five lines import the proxy modules, tell Apache that we want to proxy http://rest-api:8008 to the /api route, and set the X-Forwarded-Path header so the REST API can properly build response URLs. Make sure that rest-api matches the actual name of the Sawtooth REST API service in your docker compose file.
Modify your docker compose file
Now, to the docker compose YAML file you are running Sawtooth through, you want to add a new property under the services key:
services:
my-web-page:
build: ./path/to/web/dir/
image: my-web-page
container_name: my-web-page
volumes:
- ./path/to/web/dir/public/:/usr/local/apache2/htdocs/
expose:
- 80
ports:
- '8000:80'
depends_on:
- rest-api
This will build your Dockerfile located at ./path/to/web/dir/Dockerfile (relative to the docker compose file), and run it with its default command, which is to start up Apache. Apache will serve whatever files are located in /usr/local/apache2/htdocs/, so we'll use volumes to link the path to your web files on your host machine (i.e. ./path/to/web/dir/public/), to that directory in the container. This is basically an alias, so if you update your web app later, you don't need to restart this docker container to see the changes. Finally, ports will take the server, which is at port 80 inside the container, and forward it out to localhost:8000.
Running it all
Now you should be able to run:
docker-compose -f path/to/your/compose-file.yaml up
And it will start up your Apache server along with the Sawtooth REST API and validator and any other services you defined. If you go to http://localhost:8000, you should see your web page, and if you go to http://localhost:8000/api/blocks, you should see a JSON representation of the blocks on chain. More importantly you should be able to make the request from your web app:
request.post({
url: 'api/batches',
body: batchListBytes,
headers: { 'Content-Type': 'application/octet-stream' }
}, (err, response) => console.log(response) );
Whew. Sorry for the long response, but I'm not sure if it is possible to solve CORS any faster. Hopefully this helps.
The transaction Header should have details like, address of the block where it would be save. Here is example which I have used and is working fine for me :
String payload = "create,0001,BLockchain CPU,Black,5000";
logger.info("Sending payload as - "+ payload);
String payloadBytes = Utils.hash512(payload.getBytes()); // --fix for invaluid payload seriqalization
ByteString payloadByteString = ByteString.copyFrom(payload.getBytes());
String address = getAddress(IDEM, ITEM_ID); // get unique address for input and output
logger.info("Sending address as - "+ address);
TransactionHeader txnHeader = TransactionHeader.newBuilder().clearBatcherPublicKey()
.setBatcherPublicKey(publicKeyHex)
.setFamilyName(IDEM) // Idem Family
.setFamilyVersion(VER)
.addInputs(address)
.setNonce("1")
.addOutputs(address)
.setPayloadSha512(payloadBytes)
.setSignerPublicKey(publicKeyHex)
.build();
ByteString txnHeaderBytes = txnHeader.toByteString();
byte[] txnHeaderSignature = privateKey.signMessage(txnHeaderBytes.toString()).getBytes();
String value = Signing.sign(privateKey, txnHeader.toByteArray());
Transaction txn = Transaction.newBuilder().setHeader(txnHeaderBytes).setPayload(payloadByteString)
.setHeaderSignature(value).build();
BatchHeader batchHeader = BatchHeader.newBuilder().clearSignerPublicKey().setSignerPublicKey(publicKeyHex)
.addTransactionIds(txn.getHeaderSignature()).build();
ByteString batchHeaderBytes = batchHeader.toByteString();
byte[] batchHeaderSignature = privateKey.signMessage(batchHeaderBytes.toString()).getBytes();
String value_batch = Signing.sign(privateKey, batchHeader.toByteArray());
Batch batch = Batch.newBuilder()
.setHeader(batchHeaderBytes)
.setHeaderSignature(value_batch)
.setTrace(true)
.addTransactions(txn)
.build();
BatchList batchList = BatchList.newBuilder()
.addBatches(batch)
.build();
ByteString batchBytes = batchList.toByteString();
String serverResponse = Unirest.post("http://localhost:8008/batches")
.header("Content-Type", "application/octet-stream")
.body(batchBytes.toByteArray())
.asString()
.getBody();