tangled
alpha
login
or
join now
pierrelf.com
/
grain
0
fork
atom
Rust implementation of OCI Distribution Spec with granular access control
0
fork
atom
overview
issues
9
pulls
pipelines
add manifest retrieval
pierrelf.com
4 months ago
1ed85db4
f0324080
+160
-38
3 changed files
expand all
collapse all
unified
split
src
manifests.rs
response.rs
storage.rs
+132
-24
src/manifests.rs
···
4
4
// | end-7 | `PUT` | `/v2/<name>/manifests/<reference>` | `201` | `404` |
5
5
// | end-9 | `DELETE` | `/v2/<name>/manifests/<reference>` | `202` | `404`/`400`/`405` |
6
6
7
7
-
use serde_json::{json, Value};
7
7
+
use serde_json::Value;
8
8
use std::sync::Arc;
9
9
10
10
-
use crate::{
11
11
-
response::{not_found, not_implemented},
12
12
-
state,
13
13
-
storage::write_manifest,
14
14
-
};
10
10
+
use crate::{auth, state, storage};
15
11
use axum::{
16
12
body::Body,
17
13
extract::{Path, State},
18
18
-
http::Request,
19
19
-
response::{Json, Response},
14
14
+
http::{HeaderMap, Request, StatusCode},
15
15
+
response::Response,
20
16
};
21
17
18
18
+
fn detect_manifest_content_type(manifest_data: &[u8]) -> String {
19
19
+
if let Ok(json_str) = std::str::from_utf8(manifest_data) {
20
20
+
if let Ok(parsed) = serde_json::from_str::<Value>(json_str) {
21
21
+
if let Some(media_type) = parsed.get("mediaType").and_then(|v| v.as_str()) {
22
22
+
return media_type.to_string();
23
23
+
}
24
24
+
}
25
25
+
}
26
26
+
"application/vnd.oci.image.manifest.v1+json".to_string()
27
27
+
}
28
28
+
22
29
// end-3 GET /v2/:name/manifests/:reference
23
30
pub(crate) async fn get_manifest_by_reference(
24
24
-
State(data): State<Arc<state::App>>,
31
31
+
State(state): State<Arc<state::App>>,
25
32
Path((org, repo, reference)): Path<(String, String, String)>,
26
26
-
) -> Json<Value> {
27
27
-
let status = data.server_status.lock().await;
33
33
+
headers: HeaderMap,
34
34
+
) -> Response<Body> {
35
35
+
let host = &state.args.host;
36
36
+
37
37
+
if auth::get(State(state.clone()), headers.clone())
38
38
+
.await
39
39
+
.status()
40
40
+
!= StatusCode::OK
41
41
+
{
42
42
+
return Response::builder()
43
43
+
.status(StatusCode::UNAUTHORIZED)
44
44
+
.header(
45
45
+
"WWW-Authenticate",
46
46
+
format!("Basic realm=\"{}\", charset=\"UTF-8\"", host),
47
47
+
)
48
48
+
.body(Body::from("401 Unauthorized"))
49
49
+
.unwrap();
50
50
+
}
51
51
+
52
52
+
let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference);
53
53
+
28
54
log::info!(
29
55
"manifests/get_manifest_by_reference: org: {}, repo: {}, reference: {}",
30
56
org,
31
57
repo,
32
32
-
reference
58
58
+
clean_reference
33
59
);
34
34
-
Json(json!({
35
35
-
"not_implemented": format!("org {} repo {} reference {} server_status {}", org, repo, reference, status)
36
36
-
}))
60
60
+
61
61
+
match storage::read_manifest(&org, &repo, clean_reference) {
62
62
+
Ok(manifest_data) => {
63
63
+
let digest = sha256::digest(&manifest_data);
64
64
+
let content_type = detect_manifest_content_type(&manifest_data);
65
65
+
66
66
+
Response::builder()
67
67
+
.status(StatusCode::OK)
68
68
+
.header("Content-Length", manifest_data.len().to_string())
69
69
+
.header("Content-Type", content_type)
70
70
+
.header("Docker-Content-Digest", format!("sha256:{}", digest))
71
71
+
.body(Body::from(manifest_data))
72
72
+
.unwrap()
73
73
+
}
74
74
+
Err(e) => {
75
75
+
log::error!(
76
76
+
"Failed to read manifest {}/{}/{}: {}",
77
77
+
org,
78
78
+
repo,
79
79
+
clean_reference,
80
80
+
e
81
81
+
);
82
82
+
Response::builder()
83
83
+
.status(StatusCode::NOT_FOUND)
84
84
+
.body(Body::from("404 Not Found"))
85
85
+
.unwrap()
86
86
+
}
87
87
+
}
37
88
}
38
89
39
90
// end-3 HEAD /v2/:name/manifests/:reference
40
91
pub(crate) async fn head_manifest_by_reference(
92
92
+
State(state): State<Arc<state::App>>,
41
93
Path((org, repo, reference)): Path<(String, String, String)>,
42
42
-
) -> Response<String> {
94
94
+
headers: HeaderMap,
95
95
+
) -> Response<Body> {
96
96
+
let host = &state.args.host;
97
97
+
98
98
+
if auth::get(State(state.clone()), headers.clone())
99
99
+
.await
100
100
+
.status()
101
101
+
!= StatusCode::OK
102
102
+
{
103
103
+
return Response::builder()
104
104
+
.status(StatusCode::UNAUTHORIZED)
105
105
+
.header(
106
106
+
"WWW-Authenticate",
107
107
+
format!("Basic realm=\"{}\", charset=\"UTF-8\"", host),
108
108
+
)
109
109
+
.body(Body::from("401 Unauthorized"))
110
110
+
.unwrap();
111
111
+
}
112
112
+
113
113
+
let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference);
114
114
+
43
115
log::info!(
44
116
"manifests/head_manifest_by_reference: org: {}, repo: {}, reference: {}",
45
117
org,
46
118
repo,
47
47
-
reference
119
119
+
clean_reference
48
120
);
49
121
50
50
-
not_found()
122
122
+
if !storage::manifest_exists(&org, &repo, clean_reference) {
123
123
+
return Response::builder()
124
124
+
.status(StatusCode::NOT_FOUND)
125
125
+
.body(Body::from("404 Not Found"))
126
126
+
.unwrap();
127
127
+
}
128
128
+
129
129
+
match storage::read_manifest(&org, &repo, clean_reference) {
130
130
+
Ok(manifest_data) => {
131
131
+
let digest = sha256::digest(&manifest_data);
132
132
+
let content_type = detect_manifest_content_type(&manifest_data);
133
133
+
134
134
+
Response::builder()
135
135
+
.status(StatusCode::OK)
136
136
+
.header("Content-Length", manifest_data.len().to_string())
137
137
+
.header("Content-Type", content_type)
138
138
+
.header("Docker-Content-Digest", format!("sha256:{}", digest))
139
139
+
.body(Body::empty())
140
140
+
.unwrap()
141
141
+
}
142
142
+
Err(e) => {
143
143
+
log::error!(
144
144
+
"Failed to read manifest {}/{}/{}: {}",
145
145
+
org,
146
146
+
repo,
147
147
+
clean_reference,
148
148
+
e
149
149
+
);
150
150
+
Response::builder()
151
151
+
.status(StatusCode::NOT_FOUND)
152
152
+
.body(Body::from("404 Not Found"))
153
153
+
.unwrap()
154
154
+
}
155
155
+
}
51
156
}
52
157
53
158
// end-7 PUT /v2/:name/manifests/:reference
···
55
160
pub(crate) async fn put_manifest_by_reference(
56
161
Path((org, repo, reference)): Path<(String, String, String)>,
57
162
body: Request<Body>,
58
58
-
) -> Response<String> {
163
163
+
) -> Response {
59
164
log::info!(
60
165
"manifests/put_manifest_by_reference: org: {}, repo: {}, reference: {}",
61
166
org,
···
63
168
reference
64
169
);
65
170
66
66
-
let success = write_manifest(&org, &repo, &reference, body.into_body()).await;
171
171
+
let success = storage::write_manifest(&org, &repo, &reference, body.into_body()).await;
67
172
if !success {
68
173
return Response::builder()
69
174
.status(400)
70
70
-
.body("400 Bad Request".to_string())
175
175
+
.body(Body::from("400 Bad Request"))
71
176
.expect("Failed to build response");
72
177
}
73
178
···
77
182
"Location",
78
183
format!("/v2/{}/{}/manifests/{}", org, repo, reference),
79
184
)
80
80
-
.body("201 Created".to_string())
185
185
+
.body(Body::empty())
81
186
.expect("Failed to build response")
82
187
}
83
188
···
85
190
pub(crate) async fn delete_manifest_by_reference(
86
191
Path(name): Path<String>,
87
192
Path(reference): Path<String>,
88
88
-
) -> Response<String> {
193
193
+
) -> Response<Body> {
89
194
log::info!(
90
195
"manifests/delete_manifest_by_reference: name: {}, reference: {}",
91
196
name,
92
197
reference
93
198
);
94
94
-
not_implemented()
199
199
+
Response::builder()
200
200
+
.status(StatusCode::NOT_IMPLEMENTED)
201
201
+
.body(Body::from("501 Not Implemented"))
202
202
+
.unwrap()
95
203
}
-14
src/response.rs
···
1
1
use axum::http::Response;
2
2
3
3
-
pub(crate) fn not_found() -> Response<String> {
4
4
-
Response::builder()
5
5
-
.status(404)
6
6
-
.body("404 Not Found".to_string())
7
7
-
.unwrap()
8
8
-
}
9
9
-
10
10
-
pub(crate) fn not_implemented() -> Response<String> {
11
11
-
Response::builder()
12
12
-
.status(501)
13
13
-
.body("501 Not Implemented".to_string())
14
14
-
.unwrap()
15
15
-
}
16
16
-
17
3
pub(crate) fn unauthorized(host: &str) -> Response<String> {
18
4
Response::builder()
19
5
.status(401)
+28
src/storage.rs
···
122
122
);
123
123
std::fs::metadata(blob_path)
124
124
}
125
125
+
126
126
+
pub(crate) fn read_manifest(
127
127
+
org: &str,
128
128
+
repo: &str,
129
129
+
reference: &str,
130
130
+
) -> Result<Vec<u8>, std::io::Error> {
131
131
+
let sanitized_org = sanitize_string(org);
132
132
+
let sanitized_repo = sanitize_string(repo);
133
133
+
let sanitized_reference = sanitize_string(reference);
134
134
+
135
135
+
let manifest_path = format!(
136
136
+
"./tmp/manifests/{}/{}/{}",
137
137
+
sanitized_org, sanitized_repo, sanitized_reference
138
138
+
);
139
139
+
std::fs::read(manifest_path)
140
140
+
}
141
141
+
142
142
+
pub(crate) fn manifest_exists(org: &str, repo: &str, reference: &str) -> bool {
143
143
+
let sanitized_org = sanitize_string(org);
144
144
+
let sanitized_repo = sanitize_string(repo);
145
145
+
let sanitized_reference = sanitize_string(reference);
146
146
+
147
147
+
let manifest_path = format!(
148
148
+
"./tmp/manifests/{}/{}/{}",
149
149
+
sanitized_org, sanitized_repo, sanitized_reference
150
150
+
);
151
151
+
std::path::Path::new(&manifest_path).exists()
152
152
+
}